Skip to content

Commit 9f5db3b

Browse files
authored
feat: add @@meta and @meta attributes (#2185)
1 parent 93617a4 commit 9f5db3b

File tree

3 files changed

+100
-14
lines changed

3 files changed

+100
-14
lines changed

packages/schema/src/res/stdlib.zmodel

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,3 +748,14 @@ function raw(value: String): Any {
748748
* Marks a field to be strong-typed JSON.
749749
*/
750750
attribute @json() @@@targetField([TypeDefField])
751+
752+
753+
/**
754+
* Attaches arbitrary metadata to a model or type def.
755+
*/
756+
attribute @@meta(_ name: String, _ value: Any)
757+
758+
/**
759+
* Attaches arbitrary metadata to a field.
760+
*/
761+
attribute @meta(_ name: String, _ value: Any)

packages/sdk/src/model-meta-generator.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ import {
33
DataModel,
44
DataModelAttribute,
55
DataModelField,
6+
Expression,
67
isArrayExpr,
78
isBooleanLiteral,
89
isDataModel,
910
isDataModelField,
1011
isInvocationExpr,
1112
isNumberLiteral,
13+
isObjectExpr,
1214
isReferenceExpr,
1315
isStringLiteral,
1416
isTypeDef,
17+
ObjectExpr,
1518
ReferenceExpr,
1619
TypeDef,
1720
TypeDefField,
@@ -20,6 +23,7 @@ import type { RuntimeAttribute } from '@zenstackhq/runtime';
2023
import { lowerCaseFirst } from '@zenstackhq/runtime/local-helpers';
2124
import { streamAst } from 'langium';
2225
import { FunctionDeclarationStructure, OptionalKind, Project, VariableDeclarationKind } from 'ts-morph';
26+
import { match } from 'ts-pattern';
2327
import {
2428
CodeWriter,
2529
ExpressionContext,
@@ -362,20 +366,9 @@ function getAttributes(target: DataModelField | DataModel | TypeDefField): Runti
362366
.map((attr) => {
363367
const args: Array<{ name?: string; value: unknown }> = [];
364368
for (const arg of attr.args) {
365-
if (isNumberLiteral(arg.value)) {
366-
let v = parseInt(arg.value.value);
367-
if (isNaN(v)) {
368-
v = parseFloat(arg.value.value);
369-
}
370-
if (isNaN(v)) {
371-
throw new Error(`Invalid number literal: ${arg.value.value}`);
372-
}
373-
args.push({ name: arg.name, value: v });
374-
} else if (isStringLiteral(arg.value) || isBooleanLiteral(arg.value)) {
375-
args.push({ name: arg.name, value: arg.value.value });
376-
} else {
377-
// non-literal args are ignored
378-
}
369+
const argName = arg.$resolvedParam?.name ?? arg.name;
370+
const argValue = exprToValue(arg.value);
371+
args.push({ name: argName, value: argValue });
379372
}
380373
return { name: resolved(attr.decl).name, args };
381374
})
@@ -602,3 +595,26 @@ function getOnUpdateAction(fieldInfo: DataModelField) {
602595
}
603596
return undefined;
604597
}
598+
599+
function exprToValue(value: Expression): unknown {
600+
return match(value)
601+
.when(isStringLiteral, (v) => v.value)
602+
.when(isBooleanLiteral, (v) => v.value)
603+
.when(isNumberLiteral, (v) => {
604+
let num = parseInt(v.value);
605+
if (isNaN(num)) {
606+
num = parseFloat(v.value);
607+
}
608+
if (isNaN(num)) {
609+
return undefined;
610+
}
611+
return num;
612+
})
613+
.when(isArrayExpr, (v) => v.items.map((item) => exprToValue(item)))
614+
.when(isObjectExpr, (v) => exprToObject(v))
615+
.otherwise(() => undefined);
616+
}
617+
618+
function exprToObject(value: ObjectExpr): unknown {
619+
return Object.fromEntries(value.fields.map((field) => [field.name, exprToValue(field.value)]));
620+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { loadSchema } from '@zenstackhq/testtools';
2+
3+
describe('`@@meta` attribute tests', () => {
4+
it('generates generated into model-meta', async () => {
5+
const { modelMeta } = await loadSchema(
6+
`
7+
model User {
8+
id Int @id
9+
name String @meta('private', true) @meta('level', 1)
10+
11+
@@meta(name: 'doc', value: 'It is user model')
12+
@@meta('info', { description: 'This is a user model', geo: { country: 'US' } })
13+
}
14+
`
15+
);
16+
17+
const model = modelMeta.models.user;
18+
const userModelMeta = model.attributes.filter((attr: any) => attr.name === '@@meta');
19+
expect(userModelMeta).toEqual(
20+
expect.arrayContaining([
21+
{
22+
name: '@@meta',
23+
args: expect.arrayContaining([
24+
{ name: 'name', value: 'doc' },
25+
{ name: 'value', value: 'It is user model' },
26+
]),
27+
},
28+
{
29+
name: '@@meta',
30+
args: expect.arrayContaining([
31+
{ name: 'name', value: 'info' },
32+
{ name: 'value', value: { description: 'This is a user model', geo: { country: 'US' } } },
33+
]),
34+
},
35+
])
36+
);
37+
38+
const field = model.fields.name;
39+
const fieldMeta = field.attributes.filter((attr: any) => attr.name === '@meta');
40+
expect(fieldMeta).toEqual(
41+
expect.arrayContaining([
42+
{
43+
name: '@meta',
44+
args: expect.arrayContaining([
45+
{ name: 'name', value: 'private' },
46+
{ name: 'value', value: true },
47+
]),
48+
},
49+
{
50+
name: '@meta',
51+
args: expect.arrayContaining([
52+
{ name: 'name', value: 'level' },
53+
{ name: 'value', value: 1 },
54+
]),
55+
},
56+
])
57+
);
58+
});
59+
});

0 commit comments

Comments
 (0)