diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index 222765a29..9884d455e 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -748,3 +748,14 @@ function raw(value: String): Any { * Marks a field to be strong-typed JSON. */ attribute @json() @@@targetField([TypeDefField]) + + +/** + * Attaches arbitrary metadata to a model or type def. + */ +attribute @@meta(_ name: String, _ value: Any) + +/** + * Attaches arbitrary metadata to a field. + */ +attribute @meta(_ name: String, _ value: Any) diff --git a/packages/sdk/src/model-meta-generator.ts b/packages/sdk/src/model-meta-generator.ts index ad34fcffa..84ea5ff2e 100644 --- a/packages/sdk/src/model-meta-generator.ts +++ b/packages/sdk/src/model-meta-generator.ts @@ -3,15 +3,18 @@ import { DataModel, DataModelAttribute, DataModelField, + Expression, isArrayExpr, isBooleanLiteral, isDataModel, isDataModelField, isInvocationExpr, isNumberLiteral, + isObjectExpr, isReferenceExpr, isStringLiteral, isTypeDef, + ObjectExpr, ReferenceExpr, TypeDef, TypeDefField, @@ -20,6 +23,7 @@ import type { RuntimeAttribute } from '@zenstackhq/runtime'; import { lowerCaseFirst } from '@zenstackhq/runtime/local-helpers'; import { streamAst } from 'langium'; import { FunctionDeclarationStructure, OptionalKind, Project, VariableDeclarationKind } from 'ts-morph'; +import { match } from 'ts-pattern'; import { CodeWriter, ExpressionContext, @@ -362,20 +366,9 @@ function getAttributes(target: DataModelField | DataModel | TypeDefField): Runti .map((attr) => { const args: Array<{ name?: string; value: unknown }> = []; for (const arg of attr.args) { - if (isNumberLiteral(arg.value)) { - let v = parseInt(arg.value.value); - if (isNaN(v)) { - v = parseFloat(arg.value.value); - } - if (isNaN(v)) { - throw new Error(`Invalid number literal: ${arg.value.value}`); - } - args.push({ name: arg.name, value: v }); - } else if (isStringLiteral(arg.value) || isBooleanLiteral(arg.value)) { - args.push({ name: arg.name, value: arg.value.value }); - } else { - // non-literal args are ignored - } + const argName = arg.$resolvedParam?.name ?? arg.name; + const argValue = exprToValue(arg.value); + args.push({ name: argName, value: argValue }); } return { name: resolved(attr.decl).name, args }; }) @@ -602,3 +595,26 @@ function getOnUpdateAction(fieldInfo: DataModelField) { } return undefined; } + +function exprToValue(value: Expression): unknown { + return match(value) + .when(isStringLiteral, (v) => v.value) + .when(isBooleanLiteral, (v) => v.value) + .when(isNumberLiteral, (v) => { + let num = parseInt(v.value); + if (isNaN(num)) { + num = parseFloat(v.value); + } + if (isNaN(num)) { + return undefined; + } + return num; + }) + .when(isArrayExpr, (v) => v.items.map((item) => exprToValue(item))) + .when(isObjectExpr, (v) => exprToObject(v)) + .otherwise(() => undefined); +} + +function exprToObject(value: ObjectExpr): unknown { + return Object.fromEntries(value.fields.map((field) => [field.name, exprToValue(field.value)])); +} diff --git a/tests/integration/tests/misc/meta-annotation.test.ts b/tests/integration/tests/misc/meta-annotation.test.ts new file mode 100644 index 000000000..1c3988258 --- /dev/null +++ b/tests/integration/tests/misc/meta-annotation.test.ts @@ -0,0 +1,59 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('`@@meta` attribute tests', () => { + it('generates generated into model-meta', async () => { + const { modelMeta } = await loadSchema( + ` + model User { + id Int @id + name String @meta('private', true) @meta('level', 1) + + @@meta(name: 'doc', value: 'It is user model') + @@meta('info', { description: 'This is a user model', geo: { country: 'US' } }) + } + ` + ); + + const model = modelMeta.models.user; + const userModelMeta = model.attributes.filter((attr: any) => attr.name === '@@meta'); + expect(userModelMeta).toEqual( + expect.arrayContaining([ + { + name: '@@meta', + args: expect.arrayContaining([ + { name: 'name', value: 'doc' }, + { name: 'value', value: 'It is user model' }, + ]), + }, + { + name: '@@meta', + args: expect.arrayContaining([ + { name: 'name', value: 'info' }, + { name: 'value', value: { description: 'This is a user model', geo: { country: 'US' } } }, + ]), + }, + ]) + ); + + const field = model.fields.name; + const fieldMeta = field.attributes.filter((attr: any) => attr.name === '@meta'); + expect(fieldMeta).toEqual( + expect.arrayContaining([ + { + name: '@meta', + args: expect.arrayContaining([ + { name: 'name', value: 'private' }, + { name: 'value', value: true }, + ]), + }, + { + name: '@meta', + args: expect.arrayContaining([ + { name: 'name', value: 'level' }, + { name: 'value', value: 1 }, + ]), + }, + ]) + ); + }); +});