From d2a36382e3e27988b9ded9bbef8efa62842653d0 Mon Sep 17 00:00:00 2001 From: Christopher J Baker Date: Mon, 24 Jun 2024 20:12:52 -0700 Subject: [PATCH] restore legacy logic --- .depcheckrc | 2 +- .github/workflows/example-basic-react.yml | 7 + README.md | 26 ++- .../optimizely-as-code/events.json | 8 + .../optimizely-as-code/experiments.json | 48 +++++ .../optimizely-as-code/features.json | 17 ++ examples/basic-react/package.json | 1 + examples/basic-react/src/App.tsx | 4 +- package-lock.json | 111 ++++++------ package.json | 10 +- src/api.ts | 1 + src/commands/push-features.ts | 22 --- src/commands/sync.ts | 41 +++++ src/lib/optimizely/sync-legacy/api.ts | 95 ++++++++++ src/lib/optimizely/sync-legacy/delta.ts | 127 +++++++++++++ src/lib/optimizely/sync-legacy/index.ts | 171 ++++++++++++++++++ src/lib/optimizely/sync-legacy/interfaces.ts | 39 ++++ 17 files changed, 642 insertions(+), 88 deletions(-) create mode 100644 examples/basic-react/optimizely-as-code/events.json create mode 100644 examples/basic-react/optimizely-as-code/experiments.json create mode 100644 examples/basic-react/optimizely-as-code/features.json create mode 100644 src/api.ts delete mode 100644 src/commands/push-features.ts create mode 100644 src/commands/sync.ts create mode 100644 src/lib/optimizely/sync-legacy/api.ts create mode 100644 src/lib/optimizely/sync-legacy/delta.ts create mode 100644 src/lib/optimizely/sync-legacy/index.ts create mode 100644 src/lib/optimizely/sync-legacy/interfaces.ts diff --git a/.depcheckrc b/.depcheckrc index f4bbe2f..490195f 100644 --- a/.depcheckrc +++ b/.depcheckrc @@ -1,4 +1,4 @@ ignores: - "@types/*" - - "@oclif/*" + - "@oclif/plugin-*" - ts-node diff --git a/.github/workflows/example-basic-react.yml b/.github/workflows/example-basic-react.yml index c93835e..531f44a 100644 --- a/.github/workflows/example-basic-react.yml +++ b/.github/workflows/example-basic-react.yml @@ -42,3 +42,10 @@ jobs: - name: Build run: npm run build + + - name: Sync OptimizelyAsCode + # if: github.event_name == 'workflow_dispatch' + env: + OPTIMIZELY_ACCESS_TOKEN: my-optimizely-access-token + OPTIMIZELY_PROJECT_ID: my-optimizely-project-id + run: printenv diff --git a/README.md b/README.md index d48eba4..637b349 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,30 @@ -# optimizely-as-code +optimizely-as-code +================== optimizely-as-code +* [CLI Usage](#cli-usage) -## Usage +# CLI Usage - - + +* [`optimizely-as-code sync`](#optimizely-as-code-sync) -## Commands +## `optimizely-as-code sync` - +Push local features to Optimizely. + +``` +USAGE + $ optimizely-as-code sync --accessToken --projectId [--dry-run] + +FLAGS + --accessToken= (required) Your Optimizely access token. Can also be provided via the environment variable + OPTIMIZELY_ACCESS_TOKEN. + --dry-run Output what changes would be made without actually making the changes. + --projectId= (required) Your Optimizely Project Id. Can also be provided via the environment variable + OPTIMIZELY_PROJECT_ID. +``` diff --git a/examples/basic-react/optimizely-as-code/events.json b/examples/basic-react/optimizely-as-code/events.json new file mode 100644 index 0000000..fdd8fad --- /dev/null +++ b/examples/basic-react/optimizely-as-code/events.json @@ -0,0 +1,8 @@ +[ + { + "category": "other", + "description": "Clicking on the Learn More Button", + "event_type": "custom", + "key": "clickColor" + } +] diff --git a/examples/basic-react/optimizely-as-code/experiments.json b/examples/basic-react/optimizely-as-code/experiments.json new file mode 100644 index 0000000..49e128f --- /dev/null +++ b/examples/basic-react/optimizely-as-code/experiments.json @@ -0,0 +1,48 @@ +[ + { + "description": "Link Color Pipeline POC", + "environments": { + "development": { + "status": "running" + }, + "production": { + "status": "paused" + } + }, + "feature_key": "link_color", + "key": "link_color_experiment", + "metrics": [ + { + "aggregator": "unique", + "event_key": "clickColor", + "scope": "visitor", + "winning_direction": "increasing" + } + ], + "name": "Link Color Experiment", + "project_id": 19726420380, + "type": "feature", + "variations": [ + { + "feature_enabled": true, + "key": "blue", + "name": "Blue Link", + "status": "active", + "variable_values": { + "isOrange": "false" + }, + "weight": 5000 + }, + { + "feature_enabled": true, + "key": "orange", + "name": "Orange Link", + "status": "active", + "variable_values": { + "isOrange": "true" + }, + "weight": 5000 + } + ] + } +] diff --git a/examples/basic-react/optimizely-as-code/features.json b/examples/basic-react/optimizely-as-code/features.json new file mode 100644 index 0000000..7850695 --- /dev/null +++ b/examples/basic-react/optimizely-as-code/features.json @@ -0,0 +1,17 @@ +[ + { + "key": "link_color", + "project_id": 19726420380, + "variables": [ + { + "default_value": "false", + "key": "isOrange", + "type": "boolean" + } + ] + }, + { + "key": "demo_feature1", + "project_id": 19726420380 + } +] diff --git a/examples/basic-react/package.json b/examples/basic-react/package.json index 3b6706d..2843fb6 100644 --- a/examples/basic-react/package.json +++ b/examples/basic-react/package.json @@ -4,6 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { + "optimizely-as-code": "optimizely-as-code sync", "typecheck": "tsc --noEmit", "eslint": "eslint .", "prettier": "prettier --check .", diff --git a/examples/basic-react/src/App.tsx b/examples/basic-react/src/App.tsx index babb06a..72f070d 100644 --- a/examples/basic-react/src/App.tsx +++ b/examples/basic-react/src/App.tsx @@ -4,13 +4,15 @@ import { useDecision, } from "optimizely-as-code/react" +const userId = Math.floor(Math.random() * (10000 - 1000) + 1000).toString() + const optimizelyClient = createInstance({ sdkKey: import.meta.env.VITE_OPTIMIZELY_SDK_KEY, }) const App: React.FC = () => { return ( - + ) diff --git a/package-lock.json b/package-lock.json index 06dc771..1f809db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,15 +10,17 @@ "license": "MIT", "dependencies": { "@oclif/core": "^4.0.6", - "@oclif/plugin-help": "^6.2.3", "@optimizely/optimizely-sdk": "^5.3.3", - "@optimizely/react-sdk": "^3.1.1" + "@optimizely/react-sdk": "^3.1.1", + "cross-fetch": "^4.0.0", + "deep-equal": "^2.2.3" }, "bin": { "optimizely-as-code": "bin/run.js" }, "devDependencies": { "@bitovi/eslint-config": "^1.8.0", + "@types/deep-equal": "^1.0.4", "@types/node": "^20.14.8", "@types/react": "^18.3.3", "depcheck": "^1.4.7", @@ -2370,6 +2372,7 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-6.2.3.tgz", "integrity": "sha512-ogaCD2i6dmEgrrIKG8iV42o/s45EG53Q+dW/zeEhL8It9UdVDooXjXkZLPSyRS+CyrbK3it8Mwh08DrJxjZiqQ==", + "dev": true, "dependencies": { "@oclif/core": "^4" }, @@ -3513,6 +3516,12 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/deep-equal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/deep-equal/-/deep-equal-1.0.4.tgz", + "integrity": "sha512-tqdiS4otQP4KmY0PR3u6KbZ5EWvhNdUoS/jc93UuK23C220lOZ/9TvjfxdPcKvqwwDVtmtSCrnr0p/2dirAxkA==", + "dev": true + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -4334,7 +4343,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", - "dev": true, "dependencies": { "call-bind": "^1.0.5", "is-array-buffer": "^3.0.4" @@ -4542,7 +4550,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -4704,7 +4711,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -4960,6 +4966,14 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5126,7 +5140,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.5", @@ -5185,7 +5198,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -5202,7 +5214,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -5458,7 +5469,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, "dependencies": { "get-intrinsic": "^1.2.4" }, @@ -5470,7 +5480,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -5479,7 +5488,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -6590,7 +6598,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, "dependencies": { "is-callable": "^1.1.3" } @@ -6628,7 +6635,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6655,7 +6661,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6673,7 +6678,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -6902,7 +6906,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -6951,7 +6954,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6968,7 +6970,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -6980,7 +6981,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -6992,7 +6992,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -7004,7 +7003,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -7019,7 +7017,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -7215,7 +7212,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.0", @@ -7229,7 +7225,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -7245,7 +7240,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.1" @@ -7282,7 +7276,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, "dependencies": { "has-bigints": "^1.0.1" }, @@ -7294,7 +7287,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -7310,7 +7302,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -7352,7 +7343,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -7444,7 +7434,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -7476,7 +7465,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -7512,7 +7500,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -7537,7 +7524,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -7549,7 +7535,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", - "dev": true, "dependencies": { "call-bind": "^1.0.7" }, @@ -7576,7 +7561,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -7591,7 +7575,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -7633,7 +7616,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -7657,7 +7639,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", - "dev": true, "dependencies": { "call-bind": "^1.0.7", "get-intrinsic": "^1.2.4" @@ -7692,8 +7673,7 @@ "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" }, "node_modules/iterator.prototype": { "version": "1.1.2", @@ -8175,6 +8155,25 @@ "tslib": "^2.0.3" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/normalize-package-data": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.1.tgz", @@ -8214,7 +8213,6 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -8226,7 +8224,6 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" @@ -8242,7 +8239,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -8251,7 +8247,6 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, "dependencies": { "call-bind": "^1.0.5", "define-properties": "^1.2.1", @@ -8617,7 +8612,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -8811,7 +8805,6 @@ "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", - "dev": true, "dependencies": { "call-bind": "^1.0.6", "define-properties": "^1.2.1", @@ -9102,7 +9095,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -9119,7 +9111,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -9155,7 +9146,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", @@ -9322,7 +9312,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, "dependencies": { "internal-slot": "^1.0.4" }, @@ -9533,6 +9522,11 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -9958,11 +9952,24 @@ "defaults": "^1.0.3" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which-boxed-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, "dependencies": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", @@ -10004,7 +10011,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", @@ -10022,7 +10028,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "dev": true, "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.7", diff --git a/package.json b/package.json index 4d8cd7b..0d6f7ce 100644 --- a/package.json +++ b/package.json @@ -31,12 +31,14 @@ }, "dependencies": { "@oclif/core": "^4.0.6", - "@oclif/plugin-help": "^6.2.3", "@optimizely/optimizely-sdk": "^5.3.3", - "@optimizely/react-sdk": "^3.1.1" + "@optimizely/react-sdk": "^3.1.1", + "cross-fetch": "^4.0.0", + "deep-equal": "^2.2.3" }, "devDependencies": { "@bitovi/eslint-config": "^1.8.0", + "@types/deep-equal": "^1.0.4", "@types/node": "^20.14.8", "@types/react": "^18.3.3", "depcheck": "^1.4.7", @@ -50,8 +52,6 @@ "bin": "optimizely-as-code", "dirname": "optimizely-as-code", "commands": "./dist/commands", - "plugins": [ - "@oclif/plugin-help" - ] + "plugins": [] } } diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..336ce12 --- /dev/null +++ b/src/api.ts @@ -0,0 +1 @@ +export {} diff --git a/src/commands/push-features.ts b/src/commands/push-features.ts deleted file mode 100644 index 5dc8fa5..0000000 --- a/src/commands/push-features.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Command, Flags } from "@oclif/core" - -export default class PushFeatures extends Command { - static summary = "Push local features to Optimizely." - static description = - "An in-depth description of the command.\nIt can be multiline." - - static flags = { - dryRun: Flags.boolean({ - description: - "Outputs what changes would be made without actually making the changes.", - }), - } - - async run(): Promise { - const { args, flags } = await this.parse(PushFeatures) - - this.log( - `hello ${args.person} from ${flags.from}! (./src/commands/push-features.ts)`, - ) - } -} diff --git a/src/commands/sync.ts b/src/commands/sync.ts new file mode 100644 index 0000000..0d5bcbc --- /dev/null +++ b/src/commands/sync.ts @@ -0,0 +1,41 @@ +import { Command, Flags } from "@oclif/core" + +import sync, { Delta, EntityType } from "../lib/optimizely/sync-legacy/index.js" + +export default class SyncCommand extends Command { + static summary = "Push local features to Optimizely." + + static flags = { + accessToken: Flags.string({ + description: + "Your Optimizely access token. Can also be provided via the environment variable OPTIMIZELY_ACCESS_TOKEN.", + env: "OPTIMIZELY_ACCESS_TOKEN", + required: true, + }), + projectId: Flags.string({ + description: + "Your Optimizely Project Id. Can also be provided via the environment variable OPTIMIZELY_PROJECT_ID.", + env: "OPTIMIZELY_PROJECT_ID", + required: true, + }), + "dry-run": Flags.boolean({ + description: + "Output what changes would be made without actually making the changes.", + default: false, + }), + } + + async run(): Promise>> { + const { flags } = await this.parse(SyncCommand) + const projectPath = process.cwd() + + const deltas = await sync( + flags.accessToken, + flags.projectId, + projectPath, + flags["dry-run"], + ) + + return deltas + } +} diff --git a/src/lib/optimizely/sync-legacy/api.ts b/src/lib/optimizely/sync-legacy/api.ts new file mode 100644 index 0000000..ff0aca8 --- /dev/null +++ b/src/lib/optimizely/sync-legacy/api.ts @@ -0,0 +1,95 @@ +import fetch from "cross-fetch" + +import { BaseEntity } from "./interfaces.js" + +const OPTIMIZELY_URL = "https://api.optimizely.com/v2" + +export async function getEntities( + accessToken: string, + projectId: string, + entityName: string, +): Promise { + const url = `${OPTIMIZELY_URL}/${entityName}?project_id=${projectId}` + + const entities = await doRequest(accessToken, "GET", url) + + return entities +} + +export async function createEntities( + accessToken: string, + projectId: string, + entityName: string, + entities: Entity[], +): Promise { + let url = `${OPTIMIZELY_URL}/${entityName}` + if (entityName === "events") { + url = `${OPTIMIZELY_URL}/projects/${projectId}/custom_events` + } + + for (const entity of entities) { + await doRequest(accessToken, "POST", url, JSON.stringify(entity)) + } +} + +export async function updateEntities( + accessToken: string, + projectId: string, + entityName: string, + entities: Entity[], +): Promise { + let url = `${OPTIMIZELY_URL}/${entityName}` + if (entityName === "events") { + url = `${OPTIMIZELY_URL}/projects/${projectId}/custom_events` + } + + for (const entity of entities) { + await doRequest( + accessToken, + "PATCH", + `${url}/${entity.id}`, + JSON.stringify(entity), + ) + } +} + +export async function deleteEntities( + accessToken: string, + projectId: string, + entityName: string, + entities: Entity[], +): Promise { + let url = `${OPTIMIZELY_URL}/${entityName}` + if (entityName === "events") { + url = `${OPTIMIZELY_URL}/projects/${projectId}/custom_events` + } + + for (const entity of entities) { + await doRequest(accessToken, "DELETE", `${url}/${entity.id}`) + } +} + +async function doRequest( + accessToken: string, + method: string, + url: string, + body?: string, +) { + const response = await fetch(url, { + method, + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body, + }) + + if (!response.ok) { + const data = await response.json() + throw new Error(`Status ${data.code}: ${data.message}`) + } + + const data = await response.json() + + return data +} diff --git a/src/lib/optimizely/sync-legacy/delta.ts b/src/lib/optimizely/sync-legacy/delta.ts new file mode 100644 index 0000000..03660d6 --- /dev/null +++ b/src/lib/optimizely/sync-legacy/delta.ts @@ -0,0 +1,127 @@ +import deepEqual from "deep-equal" + +import { BaseEntity } from "./interfaces.js" + +export function getEntitiesToDelete( + entities: Entity[], + existingEntities: Entity[], +): Entity[] { + const toDelete: Entity[] = [] + for (const existingEntity of existingEntities) { + let found = false + for (const entity of entities) { + if (entity.key === existingEntity.key) { + found = true + break + } + } + + if (!found) { + toDelete.push(existingEntity) + } + } + + return toDelete +} + +export function getEntitiesToUpdate( + entities: Entity[], + existingEntities: Entity[], +): Entity[] { + const toUpdate: Entity[] = [] + for (const entity of entities) { + for (const existingEntity of existingEntities) { + if (entity.key === existingEntity.key) { + const mergedEntity = merge(existingEntity, entity) + const isEqual = deepEqual(mergedEntity, existingEntity) + if (!isEqual) { + // api breaks if given an empty page_id array + if (mergedEntity.page_ids && mergedEntity.page_ids.length === 0) { + delete mergedEntity["page_ids"] + } + + toUpdate.push(mergedEntity) + } + + break + } + } + } + + return toUpdate +} + +export function getEntitiesToCreate( + entities: Entity[], + existingEntities: Entity[], +): Entity[] { + const toCreate: Entity[] = [] + for (const entity of entities) { + let found = false + for (const existingEntity of existingEntities) { + if (entity.key === existingEntity.key) { + found = true + break + } + } + + if (!found) { + toCreate.push(entity) + } + } + + return toCreate +} + +function merge( + existingEntity: Entity, + entity: Entity, +): Entity { + const mergedEntity = { ...existingEntity, ...entity } + + if (mergedEntity.environments) { + for (const environment of Object.keys(mergedEntity.environments)) { + mergedEntity.environments[environment].environment_id = + existingEntity.environments[environment].environment_id + mergedEntity.environments[environment].environment_name = + existingEntity.environments[environment].environment_name + mergedEntity.environments[environment].percentage_included = + existingEntity.environments[environment].percentage_included + } + } + + if (mergedEntity.variations) { + for (const variation of mergedEntity.variations) { + for (const existingVariation of existingEntity.variations) { + if (variation.key === existingVariation.key) { + if (!("variation_id" in variation)) { + variation.variation_id = existingVariation.variation_id + } + + if (!("archived" in variation)) { + variation.archived = existingVariation.archived + } + + if (!("actions" in variation)) { + variation.actions = existingVariation.actions + } + break + } + } + } + } + + if (mergedEntity.variables) { + for (const variable of mergedEntity.variables) { + for (const existingVariable of existingEntity.variables) { + if (variable.key === existingVariable.key) { + variable.id = existingVariable.id + variable.archived = existingVariable.archived + break + } + } + } + } + + return mergedEntity +} diff --git a/src/lib/optimizely/sync-legacy/index.ts b/src/lib/optimizely/sync-legacy/index.ts new file mode 100644 index 0000000..6d78fd2 --- /dev/null +++ b/src/lib/optimizely/sync-legacy/index.ts @@ -0,0 +1,171 @@ +import fs from "fs" +import path from "path" + +import * as api from "./api.js" +import * as delta from "./delta.js" +import { BaseEntity, Delta } from "./interfaces.js" + +export * from "./interfaces.js" + +const entityTypes = ["features", "events", "experiments"] as const // order matters! +export type EntityType = (typeof entityTypes)[number] + +async function sync( + accessToken: string, + projectId: string, + projectPath: string, + dryRun: boolean = false, +): Promise>> { + const output: Partial> = {} + + for (const entityType of entityTypes) { + const entities = await getEntities( + accessToken, + projectId, + entityType, + projectPath, + ) + const existingEntities = await getExistingEntities( + accessToken, + projectId, + entityType, + ) + + const toCreate = delta.getEntitiesToCreate(entities, existingEntities) + const toUpdate = delta.getEntitiesToUpdate(entities, existingEntities) + const toDelete = delta.getEntitiesToDelete(entities, existingEntities) + + if (toCreate.length > 0 || toUpdate.length > 0 || toDelete.length > 0) { + const delta: Delta = {} + output[entityType] = delta + + if (toCreate.length > 0) delta.toCreate = toCreate + if (toUpdate.length > 0) delta.toUpdate = toUpdate + if (toDelete.length > 0) delta.toDelete = toDelete + } + + if (!dryRun) { + await api.createEntities(accessToken, projectId, entityType, toCreate) + await api.updateEntities(accessToken, projectId, entityType, toUpdate) + await api.deleteEntities(accessToken, projectId, entityType, toDelete) + } + } + + return output +} + +export default sync + +async function getEntities( + accessToken: string, + projectId: string, + entityType: string, + projectPath: string, +) { + const entities = getFileAsJson( + path.join(projectPath, "optimizely-as-code", `${entityType}.json`), + ) + + // find and add matching feature and event ids to experiments + if (entityType === "experiments") { + const existingFeatures = await api.getEntities( + accessToken, + projectId, + "features", + ) + const featureIdsByKey = getIdsByKey(existingFeatures) + addFeatureIdToExperiments(entities, featureIdsByKey) + + const existingEvents = await api.getEntities( + accessToken, + projectId, + "events", + ) + const eventIdsByName = getIdsByName(existingEvents) + addEventIdToExperiments(entities, eventIdsByName) + } + + return entities +} + +async function getExistingEntities( + accessToken: string, + projectId: string, + entityType: string, +) { + let existingEntities = await api.getEntities( + accessToken, + projectId, + entityType, + ) + + // remove archived entities + existingEntities = existingEntities.filter((entity) => { + if ("archived" in entity) { + return entity.archived === false + } + + if ("status" in entity) { + return entity.status !== "archived" + } + + return true + }) + + if (entityType === "features") { + for (const existingEntity of existingEntities) { + existingEntity.variables = existingEntity.variables.filter( + (variable) => variable.archived === false, + ) + } + } + + return existingEntities +} + +function getFileAsJson(path: string) { + const fileData = fs.readFileSync(path, "utf-8") + return JSON.parse(fileData) as BaseEntity[] +} + +function getIdsByName(entities: BaseEntity[]) { + const idsByName: Record = {} + for (const entity of entities) { + idsByName[entity.name] = entity.id + } + + return idsByName +} + +function getIdsByKey(entities: BaseEntity[]) { + const idsByKey: Record = {} + for (const entity of entities) { + idsByKey[entity.key] = entity.id + } + return idsByKey +} + +function addFeatureIdToExperiments( + experiments: BaseEntity[], + featureIdsByKey: Record, +) { + for (const experiment of experiments) { + if (experiment.feature_key) { + experiment.feature_id = featureIdsByKey[experiment.feature_key] + } + } +} + +function addEventIdToExperiments( + experiments: BaseEntity[], + eventIdsByName: Record, +) { + for (const experiment of experiments) { + for (const metric of experiment.metrics) { + if (metric.event_key) { + metric.event_id = eventIdsByName[metric.event_key] + delete metric["event_key"] + } + } + } +} diff --git a/src/lib/optimizely/sync-legacy/interfaces.ts b/src/lib/optimizely/sync-legacy/interfaces.ts new file mode 100644 index 0000000..0da2398 --- /dev/null +++ b/src/lib/optimizely/sync-legacy/interfaces.ts @@ -0,0 +1,39 @@ +export interface BaseEntity { + id: string + key: string + name: string + archived?: boolean + status?: string + feature_id?: string + feature_key?: string + page_ids?: unknown[] + metrics: Array<{ + event_id?: string + event_key?: string + }> + environments: Record< + string, + { + environment_id: unknown + environment_name: unknown + percentage_included: unknown + } + > + variations: Array<{ + key: string + variation_id?: unknown + archived?: unknown + actions?: unknown + }> + variables: Array<{ + key: string + id?: unknown + archived?: unknown + }> +} + +export interface Delta { + toCreate?: BaseEntity[] + toUpdate?: BaseEntity[] + toDelete?: BaseEntity[] +}