├── src ├── index.ts ├── types.ts ├── constants.ts └── cloud-config.ts ├── .npmignore ├── tsup.config.ts ├── tsconfig.json ├── LICENSE ├── package.json ├── README.md ├── .gitignore └── __test__ ├── mock-api-data.ts └── cloud-config.test.ts /src/index.ts: -------------------------------------------------------------------------------- 1 | import { cloudConfig } from "./cloud-config"; 2 | 3 | export * from "./cloud-config"; 4 | export * from "./constants"; 5 | export * from "./types"; 6 | 7 | export default cloudConfig; 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # test .npmignore by run "npm pack" before run "npm publish" 2 | vendor 3 | node_modules 4 | node_lambda_layer 5 | .serverless 6 | .env 7 | example 8 | vercel.* 9 | serverless.* 10 | *.zip 11 | *.tgz 12 | *.gz 13 | *bak.js 14 | *BAK.js 15 | test 16 | __test__ -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | target: "es2020", 5 | format: ["cjs", "esm"], // Build for commonJS and ESmodules 6 | dts: true, // Generate declaration file (.d.ts) 7 | splitting: false, 8 | sourcemap: true, 9 | clean: true, 10 | }); 11 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface CloudConfigData { 2 | projectName: string; 3 | groupName: string; 4 | featureKey: string; 5 | value: unknown; 6 | valueType: string; 7 | prodEnabled?: boolean; 8 | devEnabled?: boolean; 9 | valueEncrypted?: boolean; 10 | } 11 | 12 | export interface FetchAllConfigsParams { 13 | orgId?: string; 14 | serverSide?: boolean; 15 | accessToken?: string; 16 | cache?: RequestCache; 17 | apiPrefix?: string; 18 | cacheSeconds?: number; 19 | decryptSecret?: string; 20 | } 21 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { FetchAllConfigsParams } from "./types"; 2 | 3 | export const CLOUD_CONFIG_DEFAULT_GROUP = "defaultGroup"; 4 | export const CLOUD_CONFIG_DEFAULT_PROJECT = "defaultProject"; 5 | 6 | export const IS_PROD = process.env.NODE_ENV === "production"; 7 | 8 | export const CLOUD_CONFIG_API_ENDPOINT = 9 | process.env.NEXT_PUBLIC_CLOUD_CONFIG_API_ENDPOINT || 10 | "http://localhost:3001/api"; 11 | 12 | export const CLOUD_CONFIG_ORG_ID = 13 | process.env.NEXT_PUBLIC_CLOUD_CONFIG_ORG_ID || 14 | "Missing NEXT_PUBLIC_CLOUD_CONFIG_ORG_ID in .env"; 15 | 16 | export const CLOUD_CONFIG_CLIENT_ENCRYPT_SECRET = 17 | process.env.NEXT_PUBLIC_CLOUD_CONFIG_CLIENT_ENCRYPT_SECRET; 18 | 19 | export const CLOUD_CONFIG_SERVER_ENCRYPT_SECRET = 20 | process.env.CLOUD_CONFIG_SERVER_ENCRYPT_SECRET; 21 | 22 | export const CLOUD_CONFIG_FETCH_ALL_DEFAULT_VALUE: FetchAllConfigsParams = { 23 | orgId: CLOUD_CONFIG_ORG_ID, 24 | serverSide: false, 25 | accessToken: undefined, 26 | cache: "default", 27 | apiPrefix: CLOUD_CONFIG_API_ENDPOINT, 28 | // cacheSeconds: 60, 29 | }; 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "outDir": "./dist", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "commonjs", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["./src/*"], 20 | "~/*": ["./public/*"] 21 | }, 22 | // "incremental": true, 23 | "plugins": [ 24 | { 25 | "name": "next" 26 | } 27 | ] 28 | }, 29 | "ts-node": { 30 | // Tell ts-node CLI to install the --loader automatically 31 | "esm": true 32 | }, 33 | "include": [ 34 | "next-env.d.ts", 35 | "**/*.ts", 36 | "*.ts", 37 | "**/*.tsx", 38 | ".next/types/**/*.ts" 39 | ], 40 | "exclude": ["node_modules"], 41 | "moduleResolution": ["node_modules", ".next", "node"] 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2023 Alex Zeng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloud-configuration", 3 | "version": "0.2.0", 4 | "description": "This package allows you to use CloudConfig easily.", 5 | "author": "Alex Zeng", 6 | "license": "MIT", 7 | "main": "./dist/index.js", 8 | "module": "./dist/index.mjs", 9 | "types": "./dist/index.d.ts", 10 | "files": [ 11 | "dist", 12 | "package.json" 13 | ], 14 | "exports": { 15 | ".": { 16 | "require": "./dist/index.js", 17 | "import": "./dist/index.mjs", 18 | "types": "./dist/index.d.ts" 19 | } 20 | }, 21 | "scripts": { 22 | "build": "tsup ./src", 23 | "test": "echo \"Error: no test specified\" && exit 1" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/AlexStack/cloud-configuration.git" 28 | }, 29 | "keywords": [], 30 | "bugs": { 31 | "url": "https://github.com/AlexStack/cloud-configuration/issues" 32 | }, 33 | "homepage": "https://github.com/AlexStack/cloud-configuration#readme", 34 | "devDependencies": { 35 | "@types/crypto-js": "^4.1.2", 36 | "@types/node": "^20.8.2", 37 | "typescript": "~4", 38 | "tsup": "^8.0.2" 39 | }, 40 | "dependencies": { 41 | "crypto-js": "^4.1.1" 42 | } 43 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Global Cloud Config 2 | 3 | Manage your cloud configuration with ease to build API-driven modern web apps. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install cloud-configuration 9 | ``` 10 | 11 | or 12 | 13 | ```bash 14 | yarn add cloud-configuration 15 | ``` 16 | 17 | ## Upgrade 18 | 19 | ```bash 20 | npm update cloud-configuration 21 | ``` 22 | 23 | or 24 | 25 | ```bash 26 | yarn upgrade cloud-configuration 27 | ``` 28 | 29 | ## Basic Usage 30 | 31 | ```typescript 32 | import cloudConfig from 'cloud-configuration'; 33 | 34 | const configs = await cloudConfig.fetchAll(); 35 | 36 | // return value or null for default projectName & default groupName 37 | const auFlagUrl = cloudConfig.get({ configs, featureKey: 'au_flag_url' }); 38 | 39 | // return typed value or default value, good for TypeScript 40 | const usFlagUrl = cloudConfig.getWithDefault({ 41 | configs, 42 | featureKey: 'us_flag_url', 43 | defaultValue: 'https://example.com/us.png', 44 | }); 45 | 46 | // not default projectName & not default groupName 47 | const auFlagUrl = cloudConfig.get({ 48 | configs, 49 | projectName: 'my-project-001', 50 | groupName: 'my-group-002', 51 | featureKey: 'au_flag_url', 52 | }); 53 | ``` 54 | 55 | ## Local .env examples 56 | 57 | ```bash 58 | CLOUD_CONFIG_SERVER_ENCRYPT_SECRET=S_+2/QGV3Xz 59 | CLOUD_CONFIG_SERVER_ACCESS_TOKEN=PRIVATE_U2FsdGVkX1/V6tnPtjtXIy9pX9oVKt1M73fasTvAsFpaQtvZg== 60 | NEXT_PUBLIC_CLOUD_CONFIG_CLIENT_ENCRYPT_SECRET=C_/uTPZ+2Qrr 61 | NEXT_PUBLIC_CLOUD_CONFIG_ORG_ID=U2FsdGVkX1/1dETBp2nedJo/uDqXrpc= 62 | ``` 63 | 64 | ## Whole package size 65 | 66 | - package size: < 10 kB 67 | 68 | ## Demo projects using Global Cloud Config 69 | 70 | 75 | 76 | - [HiHB](https://hihb.com/) 77 | 78 | ## License 79 | 80 | MIT 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /__test__/mock-api-data.ts: -------------------------------------------------------------------------------- 1 | export const mockApiData = [ 2 | { 3 | projectName: 'defaultProject', 4 | groupName: 'defaultGroup', 5 | featureKey: 'json_key_1', 6 | value: 7 | 'U2FsdGVkX1+mogKJ4wd9gZ2YJ2e7s+mJXtADCnww2B1VfsSBnpyI+Y+Bwz7kpayLjkvSWEMk4iRNo38BtNKFBUrkj5lm7K0my/7xHY1KqvHA5z1msr4yAIsrJETCIl3X', 8 | valueType: 'json', 9 | valueEncrypted: true, 10 | prodEnabled: true, 11 | devEnabled: true, 12 | }, 13 | { 14 | projectName: 'defaultProject', 15 | groupName: 'defaultGroup', 16 | featureKey: 'population_auk', 17 | value: 'U2FsdGVkX1+r7+y9XWTBXxtfCQLUiryxKVyT3C298A0=', 18 | valueType: 'number', 19 | valueEncrypted: true, 20 | prodEnabled: false, 21 | devEnabled: false, 22 | }, 23 | { 24 | projectName: 'defaultProject', 25 | groupName: 'defaultGroup', 26 | featureKey: 'key1', 27 | value: 'value1', 28 | valueType: 'string', 29 | prodEnabled: true, 30 | devEnabled: true, 31 | }, 32 | { 33 | projectName: 'defaultProject', 34 | groupName: 'defaultGroup', 35 | featureKey: 'key1a', 36 | value: 'Default Value1', 37 | valueType: 'string', 38 | prodEnabled: true, 39 | devEnabled: false, 40 | }, 41 | { 42 | projectName: 'project001', 43 | groupName: 'group001', 44 | featureKey: 'encrypt_key_001', 45 | value: 46 | 'U2FsdGVkX1/r7C9jX5YV0e772hXz0dkdcz3WF+UfLooTybLFCjzVVUGVHxOPhNU0A8nf7lT45sJKD5cVIAHOyA==', 47 | valueType: 'array', 48 | valueEncrypted: true, 49 | prodEnabled: true, 50 | devEnabled: true, 51 | }, 52 | { 53 | projectName: 'project002', 54 | groupName: 'group002', 55 | featureKey: 'encrypt_key_002', 56 | value: 'U2FsdGVkX1+jmy577IqUlWs+ydML3Rs8S6LnAo1wWKG9Gk9Vvjh35rZETsW5D09f', 57 | valueType: 'array', 58 | valueEncrypted: true, 59 | prodEnabled: false, 60 | devEnabled: true, 61 | }, 62 | { 63 | projectName: 'defaultProject', 64 | groupName: 'defaultGroup', 65 | featureKey: 'boolean_1', 66 | value: true, 67 | valueType: 'boolean', 68 | prodEnabled: true, 69 | devEnabled: false, 70 | }, 71 | { 72 | projectName: 'defaultProject', 73 | groupName: 'defaultGroup', 74 | featureKey: 'boolean_2', 75 | value: false, 76 | valueType: 'boolean', 77 | prodEnabled: true, 78 | devEnabled: true, 79 | }, 80 | { 81 | projectName: 'defaultProject', 82 | groupName: 'defaultGroup', 83 | featureKey: 'boolean_003', 84 | value: true, 85 | valueType: 'boolean', 86 | prodEnabled: true, 87 | devEnabled: true, 88 | }, 89 | { 90 | projectName: 'defaultProject', 91 | groupName: 'defaultGroup', 92 | featureKey: 'float_key_1', 93 | value: 23.32, 94 | valueType: 'number', 95 | prodEnabled: false, 96 | devEnabled: false, 97 | }, 98 | { 99 | projectName: 'defaultProject', 100 | groupName: 'defaultGroup', 101 | featureKey: 'cloudConfigExtraData', 102 | value: { 103 | dataRetrievedAt: '2023-10-13T02:18:22.237Z', 104 | configCount: 10, 105 | }, 106 | valueType: 'json', 107 | prodEnabled: true, 108 | devEnabled: true, 109 | }, 110 | ]; 111 | -------------------------------------------------------------------------------- /__test__/cloud-config.test.ts: -------------------------------------------------------------------------------- 1 | import { mockApiData } from './mock-api-data'; 2 | import cloudConfig, { CloudConfigData } from '../src/index'; 3 | 4 | let cloudConfigData: CloudConfigData[] = []; 5 | 6 | const decryptSecret = 'CLIENT_/lMAHJcive'; 7 | 8 | describe('fetchAllConfigs', () => { 9 | it('should fetch configs successfully', async () => { 10 | const mockResponse = { 11 | ok: true, 12 | json: jest.fn().mockResolvedValue(mockApiData), 13 | }; 14 | const mockFetch = jest.fn().mockResolvedValue(mockResponse); 15 | global.fetch = mockFetch; 16 | 17 | cloudConfigData = await cloudConfig.fetchAll({ 18 | decryptSecret, 19 | }); 20 | 21 | expect(mockFetch).toHaveBeenCalledTimes(1); 22 | expect(mockFetch).toHaveBeenCalledWith( 23 | expect.any(String), 24 | expect.any(Object) 25 | ); 26 | expect(mockResponse.json).toHaveBeenCalledTimes(1); 27 | expect(cloudConfigData[2].featureKey).toEqual(mockApiData[2].featureKey); 28 | }); 29 | 30 | it('should throw an error when response is not ok', async () => { 31 | const mockResponse = { ok: false, status: 404, statusText: 'Not Found' }; 32 | const mockFetch = jest.fn().mockResolvedValue(mockResponse); 33 | global.fetch = mockFetch; 34 | 35 | const result = await cloudConfig.fetchAll(); 36 | expect(result.length).toEqual(0); 37 | }); 38 | 39 | it('should catch and log errors', async () => { 40 | const mockError = new Error('Test error'); 41 | const mockFetch = jest.fn().mockRejectedValue(mockError); 42 | global.fetch = mockFetch; 43 | 44 | const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); 45 | 46 | const result = await cloudConfig.fetchAll(); 47 | 48 | expect(consoleSpy).toHaveBeenCalledTimes(1); 49 | expect(consoleSpy).toHaveBeenCalledWith( 50 | '💔💔💔 fetchAllConfigs error:', 51 | mockError 52 | ); 53 | expect(result).toEqual([]); 54 | }); 55 | }); 56 | 57 | describe('getConfigWithDefaultValue', () => { 58 | it('should return the correct cloud config value', () => { 59 | const boolean_2 = cloudConfig.getWithDefault({ 60 | configs: cloudConfigData, 61 | featureKey: 'boolean_2', 62 | defaultValue: true, 63 | }); 64 | expect(boolean_2).toEqual(false); 65 | 66 | const key_not_exists = cloudConfig.getWithDefault({ 67 | configs: cloudConfigData, 68 | featureKey: 'key_not_exists', 69 | defaultValue: 'default_value_if_key_not_exists', 70 | }); 71 | expect(key_not_exists).toEqual('default_value_if_key_not_exists'); 72 | 73 | const encrypt_array_001 = cloudConfig.getWithDefault({ 74 | configs: cloudConfigData, 75 | projectName: 'project001', 76 | groupName: 'group001', 77 | featureKey: 'encrypt_key_001', 78 | defaultValue: ['encrypt_key_001_value', 'value2'], 79 | }); 80 | expect(encrypt_array_001).toEqual([ 81 | 'a1', 82 | 'a2', 83 | 's3', 84 | 's a7', 85 | 'hamilton east', 86 | ]); 87 | 88 | const encrypt_json_001 = cloudConfig.getWithDefault({ 89 | configs: cloudConfigData, 90 | featureKey: 'json_key_1', 91 | defaultValue: { message: 'json message' }, 92 | }); 93 | expect(encrypt_json_001.message).toEqual('I am JSON'); 94 | 95 | const not_enabled_key = cloudConfig.get({ 96 | configs: cloudConfigData, 97 | featureKey: 'float_key_1', 98 | }); 99 | expect(not_enabled_key).toEqual(null); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /src/cloud-config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import AES from "crypto-js/aes"; 3 | import encUtf8 from "crypto-js/enc-utf8"; 4 | 5 | import { 6 | CLOUD_CONFIG_CLIENT_ENCRYPT_SECRET, 7 | CLOUD_CONFIG_DEFAULT_GROUP, 8 | CLOUD_CONFIG_DEFAULT_PROJECT, 9 | CLOUD_CONFIG_FETCH_ALL_DEFAULT_VALUE, 10 | CLOUD_CONFIG_SERVER_ENCRYPT_SECRET, 11 | IS_PROD, 12 | } from "./constants"; 13 | import { CloudConfigData, FetchAllConfigsParams } from "./types"; 14 | 15 | export const decryptConfig = ( 16 | data: string, 17 | cryptSecret: string 18 | ): string | null => { 19 | try { 20 | const decryptedData = AES.decrypt(data, cryptSecret); 21 | const decryptedText = decryptedData.toString(encUtf8); 22 | if (!decryptedText || decryptedText === data) { 23 | return ( 24 | "Decrypt value failed! Make sure the encrypt secret is correct in env" + 25 | cryptSecret 26 | ); 27 | } 28 | return decryptedText; 29 | } catch (error) { 30 | console.log("😅😅😅 decryptConfig failed", error); 31 | } 32 | return "Decrypt value failed! Please check your encrypt secret settings in env"; 33 | }; 34 | 35 | export const parseSingleConfig = ( 36 | config: CloudConfigData, 37 | serverSideOnly = false, 38 | decryptSecret?: string 39 | ): CloudConfigData => { 40 | if (!config.valueEncrypted) { 41 | return config; 42 | } 43 | const cryptSecret = serverSideOnly 44 | ? CLOUD_CONFIG_SERVER_ENCRYPT_SECRET 45 | : CLOUD_CONFIG_CLIENT_ENCRYPT_SECRET; 46 | const newDecryptSecret = decryptSecret || cryptSecret; 47 | if (!newDecryptSecret) { 48 | // eslint-disable-next-line no-console 49 | console.log( 50 | `😅😅😅 Can't decrypt featureKey ${config.featureKey}, Please set ${ 51 | serverSideOnly 52 | ? "CLOUD_CONFIG_SERVER_ENCRYPT_SECRET" 53 | : "NEXT_PUBLIC_CLOUD_CONFIG_CLIENT_ENCRYPT_SECRET" 54 | } in .env` 55 | ); 56 | return config; 57 | } 58 | const decryptedValue = decryptConfig( 59 | config.value as string, 60 | newDecryptSecret 61 | ); 62 | if (!decryptedValue) { 63 | return config; 64 | } 65 | 66 | let newValue; 67 | if (config.valueType === "json") { 68 | try { 69 | newValue = JSON.parse(decryptedValue); 70 | } catch (error) { 71 | console.log( 72 | "😅😅😅 JSON.parse(decryptedValue) error", 73 | config.value, 74 | error 75 | ); 76 | } 77 | } 78 | if (config.valueType === "array") { 79 | newValue = decryptedValue.split(",").map((tag) => tag.trim()); 80 | } 81 | if (config.valueType === "number") { 82 | newValue = parseFloat(decryptedValue); 83 | } 84 | if (config.valueType === "boolean") { 85 | newValue = Boolean(decryptedValue); 86 | } 87 | 88 | const newConfig = { 89 | ...config, 90 | value: newValue || decryptedValue, 91 | }; 92 | 93 | return newConfig; 94 | }; 95 | 96 | export const parseAllConfigs = ( 97 | configs: CloudConfigData[], 98 | serverSideOnly = false, 99 | decryptSecret?: string 100 | ): CloudConfigData[] => { 101 | return configs.map((config) => 102 | parseSingleConfig(config, serverSideOnly, decryptSecret) 103 | ); 104 | }; 105 | 106 | interface GetCloudConfigParams { 107 | featureKey: string; 108 | groupName?: string; 109 | projectName?: string; 110 | configs: CloudConfigData[]; 111 | defaultValue?: T; 112 | } 113 | 114 | export const getCloudConfig = ({ 115 | featureKey, 116 | groupName = CLOUD_CONFIG_DEFAULT_GROUP, 117 | projectName = CLOUD_CONFIG_DEFAULT_PROJECT, 118 | configs, 119 | defaultValue, 120 | }: GetCloudConfigParams) => { 121 | const config = configs.find((item) => { 122 | if (item.featureKey !== featureKey) { 123 | return false; 124 | } 125 | if (item.groupName !== groupName) { 126 | return false; 127 | } 128 | if (item.projectName !== projectName) { 129 | return false; 130 | } 131 | return true; 132 | }); 133 | const newDefaultValue = defaultValue === undefined ? null : defaultValue; 134 | if (!config) { 135 | return newDefaultValue; 136 | } 137 | if (IS_PROD && !config.prodEnabled) { 138 | return newDefaultValue; 139 | } 140 | if (!IS_PROD && !config.devEnabled) { 141 | return newDefaultValue; 142 | } 143 | if (config.value === null || config.value === undefined) { 144 | return newDefaultValue; 145 | } 146 | 147 | return config.value as T; 148 | }; 149 | 150 | export const getConfigWithDefaultValue = ( 151 | params: GetCloudConfigParams & { defaultValue: T } 152 | ) => { 153 | const value = getCloudConfig(params); 154 | return value === null || value === undefined ? params.defaultValue : value; 155 | }; 156 | 157 | export const fetchAllConfigs = async (params?: FetchAllConfigsParams) => { 158 | try { 159 | const { 160 | orgId, 161 | serverSide, 162 | accessToken, 163 | cache, 164 | apiPrefix, 165 | cacheSeconds, 166 | decryptSecret, 167 | } = { 168 | ...CLOUD_CONFIG_FETCH_ALL_DEFAULT_VALUE, 169 | ...params, 170 | }; 171 | 172 | const startTime = Date.now(); 173 | 174 | const apiEndpoint = serverSide 175 | ? `${apiPrefix}/server-config` 176 | : `${apiPrefix}/client-config/org-${orgId}`; 177 | 178 | const requestData = serverSide 179 | ? JSON.stringify({ orgId, accessToken }) 180 | : undefined; 181 | 182 | const fetchInit = { 183 | method: serverSide ? "POST" : "GET", 184 | body: requestData, 185 | headers: { 186 | "Content-Type": "application/json", 187 | }, 188 | cache: cacheSeconds !== undefined ? undefined : cache, 189 | next: { revalidate: cacheSeconds }, 190 | }; 191 | 192 | const response = await fetch(apiEndpoint, fetchInit); 193 | if (!response.ok) { 194 | console.log("🚀 Debug fetchAllConfigs requestData:", requestData); 195 | 196 | throw new Error( 197 | `😢 fetchAllConfigs failed: ${response.status}/${response.statusText} - ${apiEndpoint}` 198 | ); 199 | } 200 | const duration = Date.now() - startTime; 201 | 202 | console.log( 203 | `fetchAllConfigs in ${(duration / 1000).toFixed(2)} seconds ${ 204 | duration > 2000 ? "💔" : "-" 205 | } ${apiEndpoint}` 206 | ); 207 | 208 | const configs = ((await response.json()) || []) as CloudConfigData[]; 209 | 210 | return parseAllConfigs(configs, serverSide, decryptSecret); 211 | } catch (error) { 212 | console.log("💔💔💔 fetchAllConfigs error:", error); 213 | } 214 | 215 | return []; 216 | }; 217 | 218 | export const cloudConfig = { 219 | // DEFAULT_GROUP: CLOUD_CONFIG_DEFAULT_GROUP, 220 | // DEFAULT_PROJECT: CLOUD_CONFIG_DEFAULT_PROJECT, 221 | // IS_PROD: IS_PROD, 222 | // API_ENDPOINT: CLOUD_CONFIG_API_ENDPOINT, 223 | decrypt: decryptConfig, 224 | parseSingle: parseSingleConfig, 225 | parseAll: parseAllConfigs, 226 | get: getCloudConfig, 227 | getWithDefault: getConfigWithDefaultValue, 228 | fetchAll: fetchAllConfigs, 229 | }; 230 | --------------------------------------------------------------------------------