Last active
March 22, 2024 19:38
-
-
Save theoephraim/ddb57d739af1d389463268403e8e0b76 to your computer and use it in GitHub Desktop.
dmno.dev configuration schema example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
defineConfigSchema, DmnoBaseTypes, NodeEnvType, configPath, dmnoFormula, switchByDmnoEnv, switchByNodeEnv, | |
valueCreatedDuringDeployment, createDmnoDataType, injectPlugin, ValidationError, registerPlugin, | |
EncryptedFileStorePlugin | |
} from '@dmno/core'; | |
import { OnePasswordDmnoPlugin } from '@dmno/1password-plugin'; | |
// plugins can be used to create reusable functionality and can reference config items in their initialization | |
const encryptedSecrets = new EncryptedFileStorePlugin({ name: 'local-secrets', key: configPath('LOCAL_SECRETS_KEY') }); | |
// pre-configured plugins can be auto-injected from those that were initialized in the workspace root | |
// just by type if there is only one instance, or with an aditional instance name if needed | |
const onePassSync = injectPlugin(OnePasswordDmnoPlugin); | |
// const onePassSync = injectPlugin('prod-vault', OnePasswordDmnoPlugin); // example with a name | |
export default defineConfigSchema({ | |
// each service can be explicitly named or will default to the name from its package.json | |
name: 'api', | |
// explicitly set a parent service to nest them, otherwise everything is a child of the "root" workspace | |
// this affects the dependency graph of services and it affects "picking" logic (see below) | |
parent: 'group1', | |
// config items can be "picked" from other services (in every service except the root) | |
// while picking from an ancestor, you can pick from _all_ config items in that service | |
// otherwise you can only pick items that have been marked as `expose: true` | |
pick: [ | |
// you can specify the source service name and key(s) to pick | |
{ | |
source: 'root', | |
key: 'SINGLE_KEY', | |
}, | |
// if source is omitted, it will fallback to the workspace root | |
{ key: 'OTHER_KEY_FROM_ROOT' }, | |
// shorthand to pick single key from root | |
'SHORTHAND_PICK_FROM_ROOT', | |
// you can pick multiple keys at once | |
{ | |
source: 'other-service', | |
key: ['MULTIPLE', 'KEYS'], | |
}, | |
// you can pick by filtering keys with a function | |
// (filters from all items or just exposed items depending if the source is an ancestor) | |
{ | |
source: 'root', | |
key: (key) => key.startsWith('DB_'), | |
}, | |
// keys can be transformed, and you can use a static value if picking a single key | |
{ | |
key: 'ORIGINAL_KEY', | |
renameKey: 'NEW_KEY_NAME', | |
}, | |
// or use a function if picking multiple | |
{ | |
key: ['KEY1', 'KEY2'], | |
renameKey: (k) => `PREFIX_${k}`, | |
}, | |
// values can also be transformed with functions | |
{ | |
key: 'GROUP1_THINGY', | |
transformValue: (v) => v + 1, | |
}, | |
], | |
// services also define more config items relevant to only themselves and to be picked by other services | |
schema: { | |
// SETTING ITEM TYPE ----------------------------------------------------------------- | |
// the default method, where a datatype is called as a function with some settings | |
EXTENDS_TYPE_INITIALIZED: { | |
extends: DmnoBaseTypes.number({ min: 0, max: 100 }) | |
}, | |
// you can use a type that has not been initialized if no settings are needed | |
EXTENDS_TYPE_UNINITIALIZED: { | |
extends: DmnoBaseTypes.number | |
}, | |
// string/named format works for some basic types (string, number, boolean, etc) with no settings | |
EXTENDS_STRING: { | |
extends: 'number' | |
}, | |
// passing nothing will try to infer the type from a static value or fallback to a string otherwise | |
DEFAULTS_TO_NUMBER: { value: 42 }, // infers number | |
DEFAULTS_TO_STRING: { value: 'cool' }, // infers string | |
FALLBACK_TO_STRING_NO_INFO: { }, // assumes string | |
FALLBACK_TO_STRING_UNABLE_TO_INFER: { // assumes string | |
value: onePassSync.item('secret-id-12345'), | |
}, | |
// an additional shorthand is provided for config items with no settings other than extends/type | |
// (although not recommended because attaching additional metadata/info is helpful) | |
SHORTHAND_TYPE_NAME: 'number', | |
SHORTHAND_TYPE_UNINITIALIZED: DmnoBaseTypes.number, | |
SHORTHAND_TYPE_INITIALIZED: DmnoBaseTypes.number({ min: 100 }), | |
// and of course you can use custom types (see below), which can in turn extend other types | |
USE_CUSTOM_TYPE: { | |
extends: MyCustomPostgresConnectionUrlType, | |
// additional settings can be added/overridden as normal | |
required: true, | |
}, | |
// SETTING VALUES ----------------------------------------------------------------- | |
// config items can specify how to set their value within their schema | |
// so you can set sensible defaults, or even set all possible values and sync secrets securely with various backends | |
// overrides from .env file(s) and actual environment variables will also be applied | |
// and then coercion/validation logic will be run on the resolved value | |
// values can be set to a static value - useful for constants and settings that will be overridden by env vars | |
STATIC_VAL: { | |
value: 'static' | |
}, | |
// or use a function that takes a ctx object that has other config item values available | |
FN_VAL: { | |
value: (ctx) => `prefix_${ctx.get('OTHER_ITEM')}` | |
}, | |
// a simple formula DSL is provided which handles common cases without needing to write a function at all | |
SET_BY_FORMULA2: { | |
value: dmnoFormula('prefix_{{ OTHER_ITEM }}'), | |
}, | |
// or synced with a secure backend using a plugin | |
SECRET_EXAMPLE: { | |
value: onePassSync.itemByReference("op://dev test/example/username"), | |
}, | |
// or switched based on another value (usually an env flag, but not always) | |
// and a "value resolver" can always return another resolver, which lets you easily compose functionality | |
// NOTE - it's easy to author your own reusable resolvers to create whatever functionality you need | |
SWITCHED_ENV_EXAMPLE: { | |
value: switchByNodeEnv({ | |
_default: 'default-value', | |
staging: (ctx) => `${ctx.get('NODE_ENV')}-value`, | |
production: onePassSync.item("asdf1234zxcv6789"), | |
}), | |
}, | |
// COMPLEX TYPE (object, arrays, maps) ////////////////////////// | |
OBJECT_EXAMPLE: { | |
extends: DmnoBaseTypes.object({ | |
CHILD1: { }, | |
CHILD2: { }, | |
}), | |
}, | |
ARRAY_EXAMPLE: { | |
extends: DmnoBaseTypes.array({ | |
itemSchema: { | |
extends: 'number' | |
}, | |
minLength: 2, | |
}), | |
}, | |
DICTIONARY_EXAMPLE: { | |
extends: DmnoBaseTypes.dictionary({ | |
itemSchema: { | |
extends: 'number' | |
}, | |
validateKeys: (key) => key.length === 2, | |
}), | |
}, | |
// VALIDATIONS + COERCION ///////////////////////////////////////////////// | |
// most validation logic will likely be handled by helpers on reusable types | |
// but sometimes you may need something more custom | |
// it will run _after_ the type (extends) defined validation(s) | |
VALIDATE_EXAMPLE: { | |
extends: DmnoBaseTypes.string({ isLength: 128, startsWith: 'pk_' }), | |
validate(val, ctx) { | |
// validations can use the ctx to access other values | |
if (ctx.get('NODE_ENV') === 'production') { | |
if (!val.startsWith('pk_live_')) { | |
// throw a ValidationError with a meaningful message | |
throw new ValidationError('production key must start with "pk_live_"'); | |
} | |
} | |
return true; | |
} | |
}, | |
// async validations can be used if a validation needs to make async calls | |
// NOTE - these will be triggered on-demand rather than run constantly like regular validations | |
ASYNC_VALIDATE_EXAMPLE: { | |
asyncValidate: async (val, ctx) => { | |
try { | |
// if the request succeeds, we know the value was ok | |
await fetch(`https://example.com/api/items/${val}`); | |
return true; | |
} catch (err) { | |
return false; | |
} | |
} | |
}, | |
// OTHER SETTINGS ////////////////////////////////////////////// | |
KITCHEN_SINK: { | |
// some basic info will help within the UI and be included in generated ts types | |
// as well as help other devs understand what this env var is for :) | |
summary: 'short label', | |
description: 'longer description can go here', | |
// mark an item as required so it will fail validation if empty | |
required: true, | |
// mark an item as secret so we know it must be handled sensitively! | |
// for example, it will not be logged or injected into front-end builds | |
secret: true, | |
// understand when this value is used, which lets us parallelize run/deploy | |
// and know when a missing item should be considered a critical problem or be ignored | |
useAt: ['build', 'boot', 'deploy'], | |
// mark an item as being "exposed" for picking by other services | |
expose: true, | |
// override name when importing/exporting into process.env | |
importEnvKey: 'IMPORT_FROM_THIS_VAR', | |
exportEnvKey: 'EXPORT_AS_THIS_VAR', | |
}, | |
}, | |
}); | |
// our custom type system allows you to build your own reusable types | |
// or to take other plugin/community defined types and tweak them as necessary | |
// internally a chain of "extends" types is stored and settings are resolved by walking up the chain | |
const MyCustomPostgresConnectionUrlType = createDmnoDataType({ | |
// you can extend one of our base types or another custom type... | |
extends: DmnoBaseTypes.url, | |
// all normal config item settings are supported | |
secret: true, | |
// a few settings are docs related and make more sense in the context of a reusable type (although they can still be set directly on items) | |
// these will show up within the UI and generated types in various ways | |
typeDescription: 'Postgres connection url', | |
externalDocs: { | |
description: 'explanation from prisma docs', | |
url: 'https://www.prisma.io/dataguide/postgresql/short-guides/connection-uris#a-quick-overview' | |
}, | |
ui: { | |
// uses iconify names, see https://icones.js.org for options | |
icon: 'akar-icons:postgresql-fill', | |
color: '336791', // postgres brand color :) | |
}, | |
// for validation/coercion, we walk up the chain and apply the functions from top to bottom | |
// for example, given the following type chain: | |
// - DmnoBaseTypes.string - makes sure the value is a string | |
// - DmnoBaseTypes.url - makes sure that string looks like a URL | |
// - PostgresConnectionUrlType - checks that url against some custom logic | |
validate(val, ctx) { | |
// check this url looks like a postgres connection url | |
}, | |
// but you can alter the exection order, or disable the parent altogether | |
runParentValidate: 'after', // set to `false` to disable running the parent's validate | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment