@slingr/cli 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +202 -0
- package/README.md +490 -319
- package/bin/dev.cmd +2 -2
- package/bin/dev.js +5 -5
- package/bin/run.cmd +2 -2
- package/bin/run.js +4 -4
- package/bin/slingr +1 -0
- package/dist/commands/build.d.ts +20 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +206 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/create-app.d.ts +0 -1
- package/dist/commands/create-app.d.ts.map +1 -1
- package/dist/commands/create-app.js +38 -57
- package/dist/commands/create-app.js.map +1 -1
- package/dist/commands/debug.d.ts +28 -0
- package/dist/commands/debug.d.ts.map +1 -0
- package/dist/commands/debug.js +474 -0
- package/dist/commands/debug.js.map +1 -0
- package/dist/commands/ds.d.ts +14 -1
- package/dist/commands/ds.d.ts.map +1 -1
- package/dist/commands/ds.js +450 -121
- package/dist/commands/ds.js.map +1 -1
- package/dist/commands/gql.d.ts +1 -1
- package/dist/commands/gql.d.ts.map +1 -1
- package/dist/commands/gql.js +190 -184
- package/dist/commands/gql.js.map +1 -1
- package/dist/commands/infra/down.d.ts.map +1 -1
- package/dist/commands/infra/down.js +8 -7
- package/dist/commands/infra/down.js.map +1 -1
- package/dist/commands/infra/up.d.ts.map +1 -1
- package/dist/commands/infra/up.js +8 -7
- package/dist/commands/infra/up.js.map +1 -1
- package/dist/commands/infra/update.d.ts +1 -0
- package/dist/commands/infra/update.d.ts.map +1 -1
- package/dist/commands/infra/update.js +33 -69
- package/dist/commands/infra/update.js.map +1 -1
- package/dist/commands/run.d.ts +29 -2
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +628 -130
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +34 -71
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/sync-metadata.d.ts +15 -0
- package/dist/commands/sync-metadata.d.ts.map +1 -0
- package/dist/commands/sync-metadata.js +225 -0
- package/dist/commands/sync-metadata.js.map +1 -0
- package/dist/commands/users.d.ts +30 -0
- package/dist/commands/users.d.ts.map +1 -0
- package/dist/commands/users.js +472 -0
- package/dist/commands/users.js.map +1 -0
- package/dist/commands/views.d.ts +11 -0
- package/dist/commands/views.d.ts.map +1 -0
- package/dist/commands/views.js +73 -0
- package/dist/commands/views.js.map +1 -0
- package/dist/projectStructure.d.ts +2 -2
- package/dist/projectStructure.d.ts.map +1 -1
- package/dist/projectStructure.js +281 -69
- package/dist/projectStructure.js.map +1 -1
- package/dist/scripts/generate-metadata.d.ts +13 -0
- package/dist/scripts/generate-metadata.d.ts.map +1 -0
- package/dist/scripts/generate-metadata.js +412 -0
- package/dist/scripts/generate-metadata.js.map +1 -0
- package/dist/scripts/generate-metadata.ts +498 -0
- package/dist/scripts/generate-schema.d.ts +1 -1
- package/dist/scripts/generate-schema.js +168 -74
- package/dist/scripts/generate-schema.js.map +1 -1
- package/dist/scripts/generate-schema.ts +258 -143
- package/dist/templates/.env.template +23 -0
- package/dist/templates/.firebaserc.template +5 -0
- package/dist/templates/.github/copilot-instructions.md.template +652 -17
- package/dist/templates/backend/Dockerfile.template +30 -0
- package/dist/templates/config/datasource.ts.template +12 -9
- package/dist/templates/config/jest.config.ts +30 -30
- package/dist/templates/config/jest.setup.ts +1 -1
- package/dist/templates/config/tsconfig.json.template +50 -29
- package/dist/templates/dataSources/mysql.ts.template +16 -13
- package/dist/templates/dataSources/postgres.ts.template +15 -13
- package/dist/templates/dataset-generator-script.ts.template +139 -139
- package/dist/templates/datasets/mysql-default/.slingr-schema.json.template +5 -0
- package/dist/templates/datasets/mysql-default/Address.jsonl.template +3 -3
- package/dist/templates/datasets/mysql-default/App.jsonl.template +4 -4
- package/dist/templates/datasets/mysql-default/Company.jsonl.template +3 -3
- package/dist/templates/datasets/mysql-default/Person.jsonl.template +2 -2
- package/dist/templates/datasets/mysql-default/User.jsonl.template +1 -0
- package/dist/templates/datasets/mysql-default/instructions.md.template +1 -0
- package/dist/templates/datasets/postgres-default/.slingr-schema.json.template +5 -0
- package/dist/templates/datasets/postgres-default/Address.jsonl.template +3 -3
- package/dist/templates/datasets/postgres-default/App.jsonl.template +4 -4
- package/dist/templates/datasets/postgres-default/Company.jsonl.template +3 -3
- package/dist/templates/datasets/postgres-default/Person.jsonl.template +2 -2
- package/dist/templates/datasets/postgres-default/User.jsonl.template +1 -0
- package/dist/templates/datasets/postgres-default/instructions.md.template +1 -0
- package/dist/templates/docker-compose.prod-test.yml.template +32 -0
- package/dist/templates/docker-compose.yml.template +24 -0
- package/dist/templates/docs/app-description.md.template +33 -33
- package/dist/templates/firebase.json.template +68 -0
- package/dist/templates/frontend/.umirc.ts.template +23 -0
- package/dist/templates/frontend/package.json.template +45 -0
- package/dist/templates/frontend/public/config.json +6 -0
- package/dist/templates/frontend/public/logo.svg +6 -0
- package/dist/templates/frontend/src/app.tsx.template +44 -0
- package/dist/templates/frontend/src/global.less.template +117 -0
- package/dist/templates/frontend/src/layouts/MainLayout.tsx.template +75 -0
- package/dist/templates/frontend/src/types/graphql-augmentation.d.ts.template +44 -0
- package/dist/templates/frontend/src/views/customViews/user/UserCreateView.tsx.template +18 -0
- package/dist/templates/frontend/src/views/customViews/user/UserEditView.tsx.template +29 -0
- package/dist/templates/frontend/src/views/customViews/user/UserReadView.tsx.template +24 -0
- package/dist/templates/frontend/src/views/customViews/user/UserTableView.tsx.template +38 -0
- package/dist/templates/frontend/src/views/customViews/welcome.tsx.template +34 -0
- package/dist/templates/frontend/tsconfig.json.template +50 -0
- package/dist/templates/gql/codegen.yml.template +25 -25
- package/dist/templates/gql/index.ts.template +17 -24
- package/dist/templates/gql/operations.graphql.template +30 -30
- package/dist/templates/ops/README.md.template +1045 -0
- package/dist/templates/ops/cloudbuild.yaml.template +161 -0
- package/dist/templates/ops/scripts/_utils.js.template +217 -0
- package/dist/templates/ops/scripts/deploy.js.template +145 -0
- package/dist/templates/ops/scripts/setup-gcp.js.template +330 -0
- package/dist/templates/ops/scripts/setup-secrets.js.template +76 -0
- package/dist/templates/ops/scripts/test-prod-local.js.template +49 -0
- package/dist/templates/package.json.template +50 -38
- package/dist/templates/pnpm-workspace.yaml.template +3 -0
- package/dist/templates/prompt-analysis.md.template +110 -110
- package/dist/templates/prompt-script-generation.md.template +258 -258
- package/dist/templates/src/Address.ts.template +28 -31
- package/dist/templates/src/App.ts.template +17 -61
- package/dist/templates/src/Company.ts.template +41 -47
- package/dist/templates/src/Models.test.ts.template +654 -654
- package/dist/templates/src/Person.test.ts.template +289 -289
- package/dist/templates/src/Person.ts.template +90 -105
- package/dist/templates/src/actions/index.ts.template +11 -11
- package/dist/templates/src/auth/permissions.ts.template +34 -0
- package/dist/templates/src/data/App.ts.template +48 -0
- package/dist/templates/src/data/User.ts.template +35 -0
- package/dist/templates/src/types/gql.d.ts.template +17 -17
- package/dist/templates/vscode/extensions.json +4 -3
- package/dist/templates/vscode/settings.json +17 -11
- package/dist/templates/workspace-package.json.template +21 -0
- package/dist/utils/buildCache.d.ts +12 -0
- package/dist/utils/buildCache.d.ts.map +1 -0
- package/dist/utils/buildCache.js +102 -0
- package/dist/utils/buildCache.js.map +1 -0
- package/dist/utils/checkFramework.d.ts +27 -0
- package/dist/utils/checkFramework.d.ts.map +1 -0
- package/dist/utils/checkFramework.js +104 -0
- package/dist/utils/checkFramework.js.map +1 -0
- package/dist/utils/datasourceParser.d.ts +11 -0
- package/dist/utils/datasourceParser.d.ts.map +1 -1
- package/dist/utils/datasourceParser.js +154 -56
- package/dist/utils/datasourceParser.js.map +1 -1
- package/dist/utils/dockerManager.d.ts +25 -0
- package/dist/utils/dockerManager.d.ts.map +1 -0
- package/dist/utils/dockerManager.js +281 -0
- package/dist/utils/dockerManager.js.map +1 -0
- package/dist/utils/infraFileParser.d.ts +26 -0
- package/dist/utils/infraFileParser.d.ts.map +1 -0
- package/dist/utils/infraFileParser.js +75 -0
- package/dist/utils/infraFileParser.js.map +1 -0
- package/dist/utils/jsonlLoader.d.ts +91 -12
- package/dist/utils/jsonlLoader.d.ts.map +1 -1
- package/dist/utils/jsonlLoader.js +674 -63
- package/dist/utils/jsonlLoader.js.map +1 -1
- package/dist/utils/model-analyzer.d.ts.map +1 -1
- package/dist/utils/model-analyzer.js +67 -13
- package/dist/utils/model-analyzer.js.map +1 -1
- package/dist/utils/userManagement.d.ts +57 -0
- package/dist/utils/userManagement.d.ts.map +1 -0
- package/dist/utils/userManagement.js +288 -0
- package/dist/utils/userManagement.js.map +1 -0
- package/dist/utils/viewsGenerator.d.ts +15 -0
- package/dist/utils/viewsGenerator.d.ts.map +1 -0
- package/dist/utils/viewsGenerator.js +311 -0
- package/dist/utils/viewsGenerator.js.map +1 -0
- package/oclif.manifest.json +445 -20
- package/package.json +29 -26
- package/src/templates/.env.template +23 -0
- package/src/templates/.firebaserc.template +5 -0
- package/src/templates/.github/copilot-instructions.md.template +652 -17
- package/src/templates/backend/Dockerfile.template +30 -0
- package/src/templates/config/datasource.ts.template +12 -9
- package/src/templates/config/jest.config.ts +30 -30
- package/src/templates/config/jest.setup.ts +1 -1
- package/src/templates/config/tsconfig.json.template +50 -29
- package/src/templates/dataSources/mysql.ts.template +16 -13
- package/src/templates/dataSources/postgres.ts.template +15 -13
- package/src/templates/dataset-generator-script.ts.template +139 -139
- package/src/templates/datasets/mysql-default/.slingr-schema.json.template +5 -0
- package/src/templates/datasets/mysql-default/Address.jsonl.template +3 -3
- package/src/templates/datasets/mysql-default/App.jsonl.template +4 -4
- package/src/templates/datasets/mysql-default/Company.jsonl.template +3 -3
- package/src/templates/datasets/mysql-default/Person.jsonl.template +2 -2
- package/src/templates/datasets/mysql-default/User.jsonl.template +1 -0
- package/src/templates/datasets/mysql-default/instructions.md.template +1 -0
- package/src/templates/datasets/postgres-default/.slingr-schema.json.template +5 -0
- package/src/templates/datasets/postgres-default/Address.jsonl.template +3 -3
- package/src/templates/datasets/postgres-default/App.jsonl.template +4 -4
- package/src/templates/datasets/postgres-default/Company.jsonl.template +3 -3
- package/src/templates/datasets/postgres-default/Person.jsonl.template +2 -2
- package/src/templates/datasets/postgres-default/User.jsonl.template +1 -0
- package/src/templates/datasets/postgres-default/instructions.md.template +1 -0
- package/src/templates/docker-compose.prod-test.yml.template +32 -0
- package/src/templates/docker-compose.yml.template +24 -0
- package/src/templates/docs/app-description.md.template +33 -33
- package/src/templates/firebase.json.template +68 -0
- package/src/templates/frontend/.umirc.ts.template +23 -0
- package/src/templates/frontend/package.json.template +45 -0
- package/src/templates/frontend/public/config.json +6 -0
- package/src/templates/frontend/public/logo.svg +6 -0
- package/src/templates/frontend/src/app.tsx.template +44 -0
- package/src/templates/frontend/src/global.less.template +117 -0
- package/src/templates/frontend/src/layouts/MainLayout.tsx.template +75 -0
- package/src/templates/frontend/src/types/graphql-augmentation.d.ts.template +44 -0
- package/src/templates/frontend/src/views/customViews/user/UserCreateView.tsx.template +18 -0
- package/src/templates/frontend/src/views/customViews/user/UserEditView.tsx.template +29 -0
- package/src/templates/frontend/src/views/customViews/user/UserReadView.tsx.template +24 -0
- package/src/templates/frontend/src/views/customViews/user/UserTableView.tsx.template +38 -0
- package/src/templates/frontend/src/views/customViews/welcome.tsx.template +34 -0
- package/src/templates/frontend/tsconfig.json.template +50 -0
- package/src/templates/gql/codegen.yml.template +25 -25
- package/src/templates/gql/index.ts.template +17 -24
- package/src/templates/gql/operations.graphql.template +30 -30
- package/src/templates/ops/README.md.template +1045 -0
- package/src/templates/ops/cloudbuild.yaml.template +161 -0
- package/src/templates/ops/scripts/_utils.js.template +217 -0
- package/src/templates/ops/scripts/deploy.js.template +145 -0
- package/src/templates/ops/scripts/setup-gcp.js.template +330 -0
- package/src/templates/ops/scripts/setup-secrets.js.template +76 -0
- package/src/templates/ops/scripts/test-prod-local.js.template +49 -0
- package/src/templates/package.json.template +50 -38
- package/src/templates/pnpm-workspace.yaml.template +3 -0
- package/src/templates/prompt-analysis.md.template +110 -110
- package/src/templates/prompt-script-generation.md.template +258 -258
- package/src/templates/src/Address.ts.template +28 -31
- package/src/templates/src/App.ts.template +17 -61
- package/src/templates/src/Company.ts.template +41 -47
- package/src/templates/src/Models.test.ts.template +654 -654
- package/src/templates/src/Person.test.ts.template +289 -289
- package/src/templates/src/Person.ts.template +90 -105
- package/src/templates/src/actions/index.ts.template +11 -11
- package/src/templates/src/auth/permissions.ts.template +34 -0
- package/src/templates/src/data/App.ts.template +48 -0
- package/src/templates/src/data/User.ts.template +35 -0
- package/src/templates/src/types/gql.d.ts.template +17 -17
- package/src/templates/vscode/extensions.json +4 -3
- package/src/templates/vscode/settings.json +17 -11
- package/src/templates/workspace-package.json.template +21 -0
- package/dist/templates/src/index.ts +0 -66
- package/src/templates/src/index.ts +0 -66
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Script to generate model and action metadata at runtime.
|
|
4
|
+
*
|
|
5
|
+
* This script is executed in the context of a user's Slingr application
|
|
6
|
+
* (copied to backend/ and run via ts-node). It loads models and actions,
|
|
7
|
+
* then uses Reflect.getMetadata() to extract field relationship info and
|
|
8
|
+
* action configuration.
|
|
9
|
+
*
|
|
10
|
+
* Output: frontend/generated/gql/metadata.ts
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Types (internal to this script)
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
interface FieldInfoData {
|
|
21
|
+
type: 'scalar' | 'reference' | 'composition' | 'sharedComposition' | 'owner' | 'file';
|
|
22
|
+
target?: string;
|
|
23
|
+
array?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ModelInfoData {
|
|
27
|
+
fields: Record<string, FieldInfoData>;
|
|
28
|
+
persistent: boolean;
|
|
29
|
+
idType?: 'string' | 'int';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ActionInfoData {
|
|
33
|
+
operation: 'query' | 'mutation';
|
|
34
|
+
type: 'model' | 'global' | 'object';
|
|
35
|
+
paramsRef?: string;
|
|
36
|
+
returnsRef?: string;
|
|
37
|
+
returnsRefs?: string[];
|
|
38
|
+
modelRef?: string;
|
|
39
|
+
label?: string;
|
|
40
|
+
bulk?: boolean;
|
|
41
|
+
// Workflow-specific UI options
|
|
42
|
+
isWorkflow?: boolean;
|
|
43
|
+
blockingExecution?: boolean;
|
|
44
|
+
successMessage?: string;
|
|
45
|
+
errorMessage?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// Metadata extraction
|
|
50
|
+
// ============================================================================
|
|
51
|
+
|
|
52
|
+
function resolveTargetClass(proto: object, fieldName: string): Function | null {
|
|
53
|
+
const typeOptions = Reflect.getMetadata('field:type:options', proto, fieldName);
|
|
54
|
+
if (typeOptions?.elementType && typeof typeOptions.elementType === 'function') {
|
|
55
|
+
try {
|
|
56
|
+
const resolved = typeOptions.elementType();
|
|
57
|
+
if (typeof resolved === 'function') {
|
|
58
|
+
return resolved;
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
/* fallback */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const designType = Reflect.getMetadata('design:type', proto, fieldName);
|
|
66
|
+
if (designType && designType !== Array && designType !== Object && typeof designType === 'function') {
|
|
67
|
+
return designType;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function detectIsArray(constructor: Function, proto: object, fieldName: string): boolean {
|
|
74
|
+
const isArrayMeta = Reflect.getMetadata('field:relationship:isArray', constructor, fieldName);
|
|
75
|
+
if (isArrayMeta === true) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const arrayFields: string[] = Reflect.getMetadata('array:field:names', constructor) || [];
|
|
80
|
+
if (arrayFields.includes(fieldName)) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const designType = Reflect.getMetadata('design:type', proto, fieldName);
|
|
85
|
+
return designType === Array;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function detectIdType(modelClass: new (...args: any[]) => any, fieldNames: string[]): 'string' | 'int' | undefined {
|
|
89
|
+
const proto = modelClass.prototype;
|
|
90
|
+
|
|
91
|
+
for (const fieldName of fieldNames) {
|
|
92
|
+
const isPrimaryKey = Reflect.getMetadata('field:primaryKey', proto, fieldName);
|
|
93
|
+
if (isPrimaryKey) {
|
|
94
|
+
const fieldType = Reflect.getMetadata('field:type', proto, fieldName);
|
|
95
|
+
if (fieldType === 'uuid' || fieldType === 'text' || fieldType === 'email') {
|
|
96
|
+
return 'string';
|
|
97
|
+
} else if (fieldType === 'integer') {
|
|
98
|
+
return 'int';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function extractModelFields(
|
|
107
|
+
modelClass: new (...args: any[]) => any,
|
|
108
|
+
getAllFieldNamesFn: (constructor: Function) => string[]
|
|
109
|
+
): Record<string, FieldInfoData> {
|
|
110
|
+
const fields: Record<string, FieldInfoData> = {};
|
|
111
|
+
const fieldNames = getAllFieldNamesFn(modelClass);
|
|
112
|
+
const proto = modelClass.prototype;
|
|
113
|
+
|
|
114
|
+
for (const fieldName of fieldNames) {
|
|
115
|
+
const relationshipType: string | undefined = Reflect.getMetadata('field:relationship:type', proto, fieldName);
|
|
116
|
+
|
|
117
|
+
if (!relationshipType) {
|
|
118
|
+
// Scalar field — check if array
|
|
119
|
+
const designType = Reflect.getMetadata('design:type', proto, fieldName);
|
|
120
|
+
const isArray = designType === Array;
|
|
121
|
+
fields[fieldName] = { type: 'scalar', ...(isArray && { array: true }) };
|
|
122
|
+
} else {
|
|
123
|
+
// Relationship field
|
|
124
|
+
const targetClass = resolveTargetClass(proto, fieldName);
|
|
125
|
+
const isArray = detectIsArray(modelClass, proto, fieldName);
|
|
126
|
+
|
|
127
|
+
// Check if target is AppFile or AppFile subclass
|
|
128
|
+
let fieldType: FieldInfoData['type'] = relationshipType as FieldInfoData['type'];
|
|
129
|
+
if (targetClass && relationshipType === 'reference') {
|
|
130
|
+
// Walk prototype chain to check if extends AppFile
|
|
131
|
+
let current = targetClass.prototype;
|
|
132
|
+
while (current) {
|
|
133
|
+
if (current.constructor.name === 'AppFile') {
|
|
134
|
+
fieldType = 'file';
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
current = Object.getPrototypeOf(current);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fields[fieldName] = {
|
|
142
|
+
type: fieldType,
|
|
143
|
+
...(targetClass && { target: targetClass.name }),
|
|
144
|
+
...(isArray && { array: true }),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return fields;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// Output generation
|
|
154
|
+
// ============================================================================
|
|
155
|
+
|
|
156
|
+
function generateModelsBlock(models: Record<string, ModelInfoData>): string {
|
|
157
|
+
const entries = Object.keys(models)
|
|
158
|
+
.sort()
|
|
159
|
+
.map(modelName => {
|
|
160
|
+
const modelInfo = models[modelName];
|
|
161
|
+
const fieldEntries = Object.entries(modelInfo.fields)
|
|
162
|
+
.map(([fn, info]) => {
|
|
163
|
+
const props: string[] = [`type: '${info.type}'`];
|
|
164
|
+
if (info.target) {
|
|
165
|
+
props.push(`target: '${info.target}'`);
|
|
166
|
+
}
|
|
167
|
+
if (info.array) {
|
|
168
|
+
props.push(`array: true`);
|
|
169
|
+
}
|
|
170
|
+
return ` ${fn}: { ${props.join(', ')} }`;
|
|
171
|
+
})
|
|
172
|
+
.join(',\n');
|
|
173
|
+
|
|
174
|
+
const modelProps: string[] = [`name: '${modelName}'`, `persistent: ${modelInfo.persistent}`];
|
|
175
|
+
|
|
176
|
+
if (modelInfo.idType) {
|
|
177
|
+
modelProps.push(`idType: '${modelInfo.idType}'`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
modelProps.push(`fields: {\n${fieldEntries}\n }`);
|
|
181
|
+
modelProps.push(`actions: {}`);
|
|
182
|
+
|
|
183
|
+
return ` ${modelName}: {\n ${modelProps.join(',\n ')},\n }`;
|
|
184
|
+
})
|
|
185
|
+
.join(',\n');
|
|
186
|
+
|
|
187
|
+
// Add WorkflowType for workflow actions
|
|
188
|
+
const workflowType = ` WorkflowType: {
|
|
189
|
+
name: 'WorkflowType',
|
|
190
|
+
persistent: false,
|
|
191
|
+
fields: {
|
|
192
|
+
id: { type: 'scalar' },
|
|
193
|
+
status: { type: 'scalar' }
|
|
194
|
+
},
|
|
195
|
+
actions: {},
|
|
196
|
+
}`;
|
|
197
|
+
|
|
198
|
+
return entries ? `${entries},\n${workflowType}` : workflowType;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function generateActionsBlock(actions: Record<string, ActionInfoData>): string {
|
|
202
|
+
const entries = Object.keys(actions)
|
|
203
|
+
.sort()
|
|
204
|
+
.map(actionName => {
|
|
205
|
+
const info = actions[actionName];
|
|
206
|
+
const props: string[] = [];
|
|
207
|
+
props.push(`operation: '${info.operation}'`);
|
|
208
|
+
props.push(`type: '${info.type}'`);
|
|
209
|
+
if (info.paramsRef) {
|
|
210
|
+
props.push(`params: modelsMetadata['${info.paramsRef}']`);
|
|
211
|
+
}
|
|
212
|
+
if (info.returnsRef) {
|
|
213
|
+
// Check if it's a primitive type (String, Number, Boolean, Date)
|
|
214
|
+
const isPrimitive = ['String', 'Number', 'Boolean', 'Date'].includes(info.returnsRef);
|
|
215
|
+
if (isPrimitive) {
|
|
216
|
+
// For primitive types, don't reference modelsMetadata (it doesn't exist)
|
|
217
|
+
// The frontend doesn't need field expansion for primitives anyway
|
|
218
|
+
props.push(`returns: { name: '${info.returnsRef}', persistent: false, fields: {}, actions: {} }`);
|
|
219
|
+
} else {
|
|
220
|
+
props.push(`returns: modelsMetadata['${info.returnsRef}']`);
|
|
221
|
+
}
|
|
222
|
+
} else if (info.returnsRefs) {
|
|
223
|
+
const refs = info.returnsRefs
|
|
224
|
+
.map(r => {
|
|
225
|
+
const isPrimitive = ['String', 'Number', 'Boolean', 'Date'].includes(r);
|
|
226
|
+
return isPrimitive
|
|
227
|
+
? `{ name: '${r}', persistent: false, fields: {}, actions: {} }`
|
|
228
|
+
: `modelsMetadata['${r}']`;
|
|
229
|
+
})
|
|
230
|
+
.join(', ');
|
|
231
|
+
props.push(`returns: [${refs}]`);
|
|
232
|
+
}
|
|
233
|
+
if (info.modelRef) {
|
|
234
|
+
props.push(`model: '${info.modelRef}'`);
|
|
235
|
+
}
|
|
236
|
+
if (info.label) {
|
|
237
|
+
props.push(`label: '${info.label}'`);
|
|
238
|
+
}
|
|
239
|
+
if (info.isWorkflow !== undefined) {
|
|
240
|
+
props.push(`isWorkflow: ${info.isWorkflow}`);
|
|
241
|
+
}
|
|
242
|
+
if (info.bulk) {
|
|
243
|
+
props.push(`bulk: ${info.bulk}`);
|
|
244
|
+
}
|
|
245
|
+
if (info.blockingExecution !== undefined) {
|
|
246
|
+
props.push(`blockingExecution: ${info.blockingExecution}`);
|
|
247
|
+
}
|
|
248
|
+
if (info.successMessage) {
|
|
249
|
+
props.push(`successMessage: '${info.successMessage}'`);
|
|
250
|
+
}
|
|
251
|
+
if (info.errorMessage) {
|
|
252
|
+
props.push(`errorMessage: '${info.errorMessage}'`);
|
|
253
|
+
}
|
|
254
|
+
return ` ${actionName}: {\n ${props.join(',\n ')},\n }`;
|
|
255
|
+
})
|
|
256
|
+
.join(',\n');
|
|
257
|
+
|
|
258
|
+
return entries;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function generateModelActionWiring(actions: Record<string, ActionInfoData>): string {
|
|
262
|
+
// Group actions by model
|
|
263
|
+
const actionsByModel: Record<string, string[]> = {};
|
|
264
|
+
for (const [actionName, info] of Object.entries(actions)) {
|
|
265
|
+
if (info.modelRef && (info.type === 'model' || info.type === 'object')) {
|
|
266
|
+
if (!actionsByModel[info.modelRef]) {
|
|
267
|
+
actionsByModel[info.modelRef] = [];
|
|
268
|
+
}
|
|
269
|
+
actionsByModel[info.modelRef].push(actionName);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const lines: string[] = [];
|
|
274
|
+
for (const [modelName, actionNames] of Object.entries(actionsByModel).sort(([a], [b]) => a.localeCompare(b))) {
|
|
275
|
+
const actionEntries = actionNames
|
|
276
|
+
.sort()
|
|
277
|
+
.map(name => ` ${name}: actionsMetadata['${name}']`)
|
|
278
|
+
.join(',\n');
|
|
279
|
+
lines.push(`modelsMetadata['${modelName}'].actions = {\n${actionEntries},\n};`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return lines.join('\n\n');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function generateOutputFile(models: Record<string, ModelInfoData>, actions: Record<string, ActionInfoData>): string {
|
|
286
|
+
const modelsBlock = generateModelsBlock(models);
|
|
287
|
+
const actionsBlock = generateActionsBlock(actions);
|
|
288
|
+
const wiringBlock = generateModelActionWiring(actions);
|
|
289
|
+
|
|
290
|
+
return `/**
|
|
291
|
+
* Models & Actions Metadata - Auto-generated by Slingr CLI
|
|
292
|
+
*
|
|
293
|
+
* This file provides model field metadata and action metadata for use by
|
|
294
|
+
* the QueryBuilder, OperationBuilder, and other framework components.
|
|
295
|
+
*
|
|
296
|
+
* DO NOT EDIT THIS FILE MANUALLY - regenerate with: slingr gql generate-sdk
|
|
297
|
+
*/
|
|
298
|
+
|
|
299
|
+
import type {
|
|
300
|
+
ModelsMetadata,
|
|
301
|
+
ActionsMetadata,
|
|
302
|
+
} from '@slingr/framework-frontend';
|
|
303
|
+
|
|
304
|
+
// Phase 1: Models (actions populated in phase 3)
|
|
305
|
+
export const modelsMetadata: ModelsMetadata = {
|
|
306
|
+
${modelsBlock}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// Phase 2: Actions (reference models directly)
|
|
310
|
+
export const actionsMetadata = {
|
|
311
|
+
${actionsBlock}
|
|
312
|
+
} satisfies ActionsMetadata;
|
|
313
|
+
|
|
314
|
+
// Augment global registry so \`ActionName\` (from @slingr/framework-frontend) resolves
|
|
315
|
+
// to a specific union of action name literals rather than plain \`string\`.
|
|
316
|
+
declare global {
|
|
317
|
+
interface SlingrActionRegistry extends Record<keyof typeof actionsMetadata, true> {}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Phase 3: Wire model → action references (model + object actions only)
|
|
321
|
+
${wiringBlock}
|
|
322
|
+
`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ============================================================================
|
|
326
|
+
// Main
|
|
327
|
+
// ============================================================================
|
|
328
|
+
|
|
329
|
+
export async function generateMetadataFile() {
|
|
330
|
+
try {
|
|
331
|
+
console.log('🔄 Generating metadata from model/action decorators...');
|
|
332
|
+
|
|
333
|
+
// Dynamic import to avoid compile-time dependency on @slingr/framework-backend
|
|
334
|
+
// This script runs in the user's backend context where @slingr/framework-backend is installed
|
|
335
|
+
// @ts-ignore
|
|
336
|
+
const slingrFramework: any = await import('@slingr/framework-backend');
|
|
337
|
+
const { getAllModels, getAllActions, getAllFieldNames } = slingrFramework;
|
|
338
|
+
|
|
339
|
+
// Extract model metadata
|
|
340
|
+
const allModels = getAllModels();
|
|
341
|
+
const modelsMetadata: Record<string, ModelInfoData> = {};
|
|
342
|
+
|
|
343
|
+
for (const modelClass of allModels) {
|
|
344
|
+
const fields = extractModelFields(modelClass, getAllFieldNames);
|
|
345
|
+
const dataSource = Reflect.getMetadata('model:dataSource', modelClass);
|
|
346
|
+
const isPersistent = !!dataSource;
|
|
347
|
+
|
|
348
|
+
if (Object.keys(fields).length > 0) {
|
|
349
|
+
const metadata: ModelInfoData = {
|
|
350
|
+
fields,
|
|
351
|
+
persistent: isPersistent,
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
if (isPersistent) {
|
|
355
|
+
const fieldNames = getAllFieldNames(modelClass);
|
|
356
|
+
const idType = detectIdType(modelClass, fieldNames);
|
|
357
|
+
if (idType) {
|
|
358
|
+
metadata.idType = idType;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
modelsMetadata[modelClass.name] = metadata;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Extract action metadata (only api: 'gql')
|
|
367
|
+
const allActions = getAllActions();
|
|
368
|
+
const actionsMetadata: Record<string, ActionInfoData> = {};
|
|
369
|
+
|
|
370
|
+
for (const { actionClass, options, actionType } of allActions) {
|
|
371
|
+
if (options.api !== 'gql') {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Ensure params model is in modelsMetadata
|
|
376
|
+
if (options.params && !modelsMetadata[options.params.name]) {
|
|
377
|
+
const fields = extractModelFields(options.params, getAllFieldNames);
|
|
378
|
+
const dataSource = Reflect.getMetadata('model:dataSource', options.params);
|
|
379
|
+
const isPersistent = !!dataSource;
|
|
380
|
+
|
|
381
|
+
if (Object.keys(fields).length > 0) {
|
|
382
|
+
const metadata: ModelInfoData = {
|
|
383
|
+
fields,
|
|
384
|
+
persistent: isPersistent,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
if (isPersistent) {
|
|
388
|
+
const fieldNames = getAllFieldNames(options.params);
|
|
389
|
+
const idType = detectIdType(options.params, fieldNames);
|
|
390
|
+
if (idType) {
|
|
391
|
+
metadata.idType = idType;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
modelsMetadata[options.params.name] = metadata;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Ensure returns model(s) are in modelsMetadata
|
|
400
|
+
const returnsArr = Array.isArray(options.returns) ? options.returns : options.returns ? [options.returns] : [];
|
|
401
|
+
for (const ret of returnsArr) {
|
|
402
|
+
if (!modelsMetadata[ret.name]) {
|
|
403
|
+
const fields = extractModelFields(ret, getAllFieldNames);
|
|
404
|
+
const dataSource = Reflect.getMetadata('model:dataSource', ret);
|
|
405
|
+
const isPersistent = !!dataSource;
|
|
406
|
+
|
|
407
|
+
if (Object.keys(fields).length > 0) {
|
|
408
|
+
const metadata: ModelInfoData = {
|
|
409
|
+
fields,
|
|
410
|
+
persistent: isPersistent,
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
if (isPersistent) {
|
|
414
|
+
const fieldNames = getAllFieldNames(ret);
|
|
415
|
+
const idType = detectIdType(ret, fieldNames);
|
|
416
|
+
if (idType) {
|
|
417
|
+
metadata.idType = idType;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
modelsMetadata[ret.name] = metadata;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Extract UI options (handle both single object and contextual array)
|
|
427
|
+
const uiOptions = Array.isArray(options.ui) ? options.ui[0] : options.ui;
|
|
428
|
+
|
|
429
|
+
const isWorkflow = options.workflow === true;
|
|
430
|
+
const isBulk = options.bulk === true || uiOptions?.bulk === true;
|
|
431
|
+
// Default blockingExecution to true for workflow and bulk actions — both block the UI by design
|
|
432
|
+
const isBlockingByDefault = isWorkflow || isBulk;
|
|
433
|
+
const blockingExecution = uiOptions?.blockingExecution ?? isBlockingByDefault;
|
|
434
|
+
|
|
435
|
+
const info: ActionInfoData = {
|
|
436
|
+
operation: options.type === 'read' ? 'query' : 'mutation',
|
|
437
|
+
type: actionType,
|
|
438
|
+
label: uiOptions?.label,
|
|
439
|
+
isWorkflow,
|
|
440
|
+
blockingExecution,
|
|
441
|
+
successMessage: uiOptions?.successMessage,
|
|
442
|
+
errorMessage: uiOptions?.errorMessage,
|
|
443
|
+
};
|
|
444
|
+
if (isBulk) {
|
|
445
|
+
info.bulk = true;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (options.params) {
|
|
449
|
+
info.paramsRef = options.params.name;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (options.returns) {
|
|
453
|
+
// For workflows, the actual GraphQL return type is WorkflowType (not the declared type)
|
|
454
|
+
if (options.workflow === true) {
|
|
455
|
+
info.returnsRef = 'WorkflowType';
|
|
456
|
+
} else if (Array.isArray(options.returns)) {
|
|
457
|
+
info.returnsRefs = options.returns.map((cls: Function) => cls.name);
|
|
458
|
+
} else {
|
|
459
|
+
info.returnsRef = options.returns.name;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (options.model) {
|
|
464
|
+
info.modelRef = options.model.name;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
actionsMetadata[actionClass.name] = info;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Generate output
|
|
471
|
+
const outputContent = generateOutputFile(modelsMetadata, actionsMetadata);
|
|
472
|
+
|
|
473
|
+
// Write to frontend/generated/gql/metadata.ts
|
|
474
|
+
const frontendPath = path.resolve(process.cwd(), '..', 'frontend');
|
|
475
|
+
const outputDir = path.join(frontendPath, 'generated', 'gql');
|
|
476
|
+
const outputPath = path.join(outputDir, 'metadata.ts');
|
|
477
|
+
|
|
478
|
+
if (!fs.existsSync(outputDir)) {
|
|
479
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
fs.writeFileSync(outputPath, outputContent, 'utf8');
|
|
483
|
+
|
|
484
|
+
const modelCount = Object.keys(modelsMetadata).length;
|
|
485
|
+
const actionCount = Object.keys(actionsMetadata).length;
|
|
486
|
+
const fieldCount = Object.values(modelsMetadata).reduce(
|
|
487
|
+
(sum, modelInfo) => sum + Object.keys(modelInfo.fields).length,
|
|
488
|
+
0
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
console.log(` Found ${modelCount} models with ${fieldCount} fields and ${actionCount} actions`);
|
|
492
|
+
console.log(`✅ Metadata generated: ${outputPath}`);
|
|
493
|
+
} catch (error: any) {
|
|
494
|
+
console.error(`❌ Failed to generate metadata: ${error.message}`);
|
|
495
|
+
console.error(error);
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Script to generate GraphQL schema from slingr-
|
|
3
|
+
* Script to generate GraphQL schema from @slingr/framework-backend
|
|
4
4
|
* This script is executed in the context of a user's Slingr application
|
|
5
5
|
*
|
|
6
6
|
* By default, the script generates the schema and exits immediately.
|