@slingr/cli 0.0.2 → 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 -27
- 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
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -7,15 +40,19 @@ exports.JsonlDatasetLoader = void 0;
|
|
|
7
40
|
exports.discoverModels = discoverModels;
|
|
8
41
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
42
|
const node_path_1 = __importDefault(require("node:path"));
|
|
43
|
+
const framework_backend_1 = require("@slingr/framework-backend");
|
|
10
44
|
require("reflect-metadata");
|
|
45
|
+
const cliProgress = __importStar(require("cli-progress"));
|
|
11
46
|
/**
|
|
12
47
|
* Utility class for loading datasets from JSONL files using Slingr model fromJSON() functionality
|
|
13
48
|
*/
|
|
14
49
|
class JsonlDatasetLoader {
|
|
50
|
+
/** Per-run cache to avoid repeated DB lookups for the same referenced record. */
|
|
51
|
+
dbLookupCache = new Map();
|
|
15
52
|
/**
|
|
16
53
|
* Analyze model dependencies to determine the correct loading order
|
|
17
54
|
*/
|
|
18
|
-
analyzeDependencies(modelMap, verbose = false) {
|
|
55
|
+
analyzeDependencies(modelMap, verbose = false, availableModels) {
|
|
19
56
|
const dependencies = {};
|
|
20
57
|
// Initialize dependency tracking for all models
|
|
21
58
|
for (const modelName of Object.keys(modelMap)) {
|
|
@@ -27,7 +64,7 @@ class JsonlDatasetLoader {
|
|
|
27
64
|
}
|
|
28
65
|
// Analyze each model for dependencies
|
|
29
66
|
for (const [modelName, ModelClass] of Object.entries(modelMap)) {
|
|
30
|
-
const modelDeps = this.extractModelDependencies(ModelClass, verbose);
|
|
67
|
+
const modelDeps = this.extractModelDependencies(ModelClass, verbose, availableModels);
|
|
31
68
|
dependencies[modelName].dependencies = modelDeps;
|
|
32
69
|
// Update dependents for referenced models
|
|
33
70
|
for (const depName of modelDeps) {
|
|
@@ -50,7 +87,7 @@ class JsonlDatasetLoader {
|
|
|
50
87
|
/**
|
|
51
88
|
* Extract dependencies from a model class by analyzing its decorators/metadata
|
|
52
89
|
*/
|
|
53
|
-
extractModelDependencies(ModelClass, verbose = false) {
|
|
90
|
+
extractModelDependencies(ModelClass, verbose = false, availableModels) {
|
|
54
91
|
const dependencies = [];
|
|
55
92
|
if (verbose) {
|
|
56
93
|
console.log(` 🔍 Analyzing ${ModelClass.name} for dependencies...`);
|
|
@@ -60,9 +97,17 @@ class JsonlDatasetLoader {
|
|
|
60
97
|
const instance = new ModelClass();
|
|
61
98
|
// Get all property descriptors from the prototype and instance
|
|
62
99
|
const proto = Object.getPrototypeOf(instance);
|
|
63
|
-
const
|
|
100
|
+
const prototypeProps = Object.getOwnPropertyNames(proto).concat(Object.getOwnPropertyNames(instance));
|
|
101
|
+
// Also get fields from MODEL_FIELDS metadata (stores all decorated field names)
|
|
102
|
+
// This is critical for fields like @ReferenceField that may not have values assigned
|
|
103
|
+
const modelFields = Reflect.getMetadata?.('model:fields', ModelClass) || [];
|
|
104
|
+
// Combine and deduplicate property names
|
|
105
|
+
const propertyNames = [...new Set([...prototypeProps, ...modelFields])];
|
|
64
106
|
if (verbose) {
|
|
65
107
|
console.log(` Properties found:`, propertyNames.filter(p => p !== 'constructor'));
|
|
108
|
+
if (modelFields.length > 0) {
|
|
109
|
+
console.log(` Model fields from metadata:`, modelFields);
|
|
110
|
+
}
|
|
66
111
|
}
|
|
67
112
|
for (const propertyName of propertyNames) {
|
|
68
113
|
if (propertyName === 'constructor') {
|
|
@@ -87,46 +132,63 @@ class JsonlDatasetLoader {
|
|
|
87
132
|
}
|
|
88
133
|
// Check for Reference and Composition relationships
|
|
89
134
|
if (fieldType === 'relationship' && relationshipType && designType) {
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (verbose) {
|
|
94
|
-
console.log(` ✅ Found dependency: ${ModelClass.name}.${propertyName} → ${designType} (${relationshipType})`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
else if (typeof designType === 'function' && designType.name) {
|
|
98
|
-
dependencies.push(designType.name);
|
|
135
|
+
// Skip regular compositions - in compositions, the child depends on the parent, not the other way around
|
|
136
|
+
// The child's @OwnerReferenceField creates the dependency from child to parent
|
|
137
|
+
if (relationshipType === 'composition') {
|
|
99
138
|
if (verbose) {
|
|
100
|
-
console.log(`
|
|
139
|
+
console.log(` ⏭️ Skipping composition: ${ModelClass.name}.${propertyName} → ${designType?.name || designType} (child depends on parent, not parent on child)`);
|
|
101
140
|
}
|
|
141
|
+
continue;
|
|
102
142
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (referencedType && referencedType.name) {
|
|
113
|
-
dependencies.push(referencedType.name);
|
|
143
|
+
// For references and sharedCompositions, check if the target has a JSONL file
|
|
144
|
+
// Only add as dependency if the file exists (meaning it's loaded separately)
|
|
145
|
+
const targetName = typeof designType === 'string' ? designType : designType.name;
|
|
146
|
+
// If availableModels is provided, only add dependency if the model has a JSONL file
|
|
147
|
+
// This allows compositions to be embedded OR loaded from separate files
|
|
148
|
+
if (targetName) {
|
|
149
|
+
const shouldAddDependency = !availableModels || availableModels.has(targetName);
|
|
150
|
+
if (shouldAddDependency) {
|
|
151
|
+
dependencies.push(targetName);
|
|
114
152
|
if (verbose) {
|
|
115
|
-
console.log(` Found
|
|
153
|
+
console.log(` ✅ Found dependency: ${ModelClass.name}.${propertyName} → ${targetName} (${relationshipType}, has JSONL file)`);
|
|
116
154
|
}
|
|
117
155
|
}
|
|
156
|
+
else if (verbose) {
|
|
157
|
+
console.log(` ⏭️ Skipping dependency: ${ModelClass.name}.${propertyName} → ${targetName} (${relationshipType}, no JSONL file - will use embedded data)`);
|
|
158
|
+
}
|
|
118
159
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
160
|
+
}
|
|
161
|
+
// Check field:type:options for elementType (handles array references and compositions)
|
|
162
|
+
// This is the correct way to get the target class for relationships in the Slingr framework
|
|
163
|
+
const fieldOptions = Reflect.getMetadata?.('field:type:options', proto, propertyName);
|
|
164
|
+
if (fieldOptions?.elementType && typeof fieldOptions.elementType === 'function') {
|
|
165
|
+
try {
|
|
166
|
+
const targetClass = fieldOptions.elementType();
|
|
167
|
+
if (targetClass?.name && !dependencies.includes(targetClass.name)) {
|
|
168
|
+
// Skip if this is a regular composition field
|
|
169
|
+
if (relationshipType === 'composition') {
|
|
170
|
+
if (verbose) {
|
|
171
|
+
console.log(` ⏭️ Skipping composition via type: ${ModelClass.name}.${propertyName} → ${targetClass.name}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// Only add dependency if the model has a JSONL file
|
|
176
|
+
const shouldAddDependency = !availableModels || availableModels.has(targetClass.name);
|
|
177
|
+
if (shouldAddDependency) {
|
|
178
|
+
dependencies.push(targetClass.name);
|
|
179
|
+
if (verbose) {
|
|
180
|
+
console.log(` ✅ Found dependency via type: ${ModelClass.name}.${propertyName} → ${targetClass.name} (has JSONL file)`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else if (verbose) {
|
|
184
|
+
console.log(` ⏭️ Skipping dependency via type: ${ModelClass.name}.${propertyName} → ${targetClass.name} (no JSONL file - embedded)`);
|
|
185
|
+
}
|
|
127
186
|
}
|
|
128
187
|
}
|
|
129
188
|
}
|
|
189
|
+
catch {
|
|
190
|
+
// elementType() can fail if referenced class is not loaded yet, ignore
|
|
191
|
+
}
|
|
130
192
|
}
|
|
131
193
|
}
|
|
132
194
|
catch (error) {
|
|
@@ -145,6 +207,29 @@ class JsonlDatasetLoader {
|
|
|
145
207
|
// Remove duplicates and self-references
|
|
146
208
|
return Array.from(new Set(dependencies)).filter(dep => dep !== ModelClass.name);
|
|
147
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Get the referenced model name from field metadata
|
|
212
|
+
*/
|
|
213
|
+
getReferencedModelName(ModelClass, fieldName) {
|
|
214
|
+
const designType = Reflect.getMetadata('design:type', ModelClass.prototype, fieldName);
|
|
215
|
+
if (designType && designType.name && designType.name !== 'Array') {
|
|
216
|
+
return designType.name;
|
|
217
|
+
}
|
|
218
|
+
// Try elementType for arrays
|
|
219
|
+
const fieldOptions = Reflect.getMetadata('field:type:options', ModelClass.prototype, fieldName);
|
|
220
|
+
if (fieldOptions?.elementType && typeof fieldOptions.elementType === 'function') {
|
|
221
|
+
try {
|
|
222
|
+
const targetClass = fieldOptions.elementType();
|
|
223
|
+
if (targetClass?.name) {
|
|
224
|
+
return targetClass.name;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Ignore
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
148
233
|
/**
|
|
149
234
|
* Perform topological sort to determine loading order
|
|
150
235
|
*/
|
|
@@ -154,6 +239,9 @@ class JsonlDatasetLoader {
|
|
|
154
239
|
const result = [];
|
|
155
240
|
const visit = (modelName) => {
|
|
156
241
|
if (visiting.has(modelName)) {
|
|
242
|
+
console.error(`\n❌ Circular dependency detected!`);
|
|
243
|
+
console.error(`Currently visiting chain:`, Array.from(visiting));
|
|
244
|
+
console.error(`Trying to visit again:`, modelName);
|
|
157
245
|
throw new Error(`Circular dependency detected involving: ${modelName}`);
|
|
158
246
|
}
|
|
159
247
|
if (visited.has(modelName)) {
|
|
@@ -183,33 +271,99 @@ class JsonlDatasetLoader {
|
|
|
183
271
|
* Load all JSONL files from a dataset directory
|
|
184
272
|
*/
|
|
185
273
|
async loadDataset(options) {
|
|
186
|
-
const { datasetPath, modelMap, validateRecords = true, verbose = false } = options;
|
|
274
|
+
const { datasetPath, modelMap, validateRecords = true, verbose = false, includeModels, excludeModels, dataSource, } = options;
|
|
275
|
+
// Reset per-run cache so stale entries from a previous call don't bleed through.
|
|
276
|
+
this.dbLookupCache.clear();
|
|
187
277
|
if (!(await fs_extra_1.default.pathExists(datasetPath))) {
|
|
188
278
|
throw new Error(`Dataset directory not found: ${datasetPath}`);
|
|
189
279
|
}
|
|
190
|
-
//
|
|
191
|
-
const dependencyAnalysis = this.analyzeDependencies(modelMap, verbose);
|
|
192
|
-
// Find all JSONL files in the dataset directory
|
|
280
|
+
// Find all JSONL files in the dataset directory first
|
|
193
281
|
const files = await fs_extra_1.default.readdir(datasetPath);
|
|
194
|
-
|
|
282
|
+
let jsonlFiles = files.filter(f => f.endsWith('.jsonl'));
|
|
283
|
+
if (verbose) {
|
|
284
|
+
console.log(`📁 Found ${jsonlFiles.length} JSONL files in dataset directory:`, jsonlFiles.map(f => f.replace(/\.jsonl$/i, '')));
|
|
285
|
+
}
|
|
286
|
+
// Apply model filtering before dependency analysis (case-insensitive)
|
|
287
|
+
if (includeModels && includeModels.length > 0) {
|
|
288
|
+
// Create a case-insensitive lookup map: lowercase -> original model name
|
|
289
|
+
const includeLookup = new Map(includeModels.map(name => [name.toLowerCase(), name]));
|
|
290
|
+
if (verbose) {
|
|
291
|
+
console.log(`🔍 Filtering to include only:`, includeModels);
|
|
292
|
+
}
|
|
293
|
+
jsonlFiles = jsonlFiles.filter(f => {
|
|
294
|
+
const modelName = f.replace(/\.jsonl$/i, '');
|
|
295
|
+
const matched = includeLookup.has(modelName.toLowerCase());
|
|
296
|
+
if (verbose && !matched) {
|
|
297
|
+
console.log(` ⏭️ Skipping ${f} (not in include list)`);
|
|
298
|
+
}
|
|
299
|
+
return matched;
|
|
300
|
+
});
|
|
301
|
+
if (verbose) {
|
|
302
|
+
console.log(`✅ Filtered to ${jsonlFiles.length} files:`, jsonlFiles.map(f => f.replace(/\.jsonl$/i, '')));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else if (excludeModels && excludeModels.length > 0) {
|
|
306
|
+
// Create a case-insensitive lookup set
|
|
307
|
+
const excludeLookup = new Set(excludeModels.map(name => name.toLowerCase()));
|
|
308
|
+
if (verbose) {
|
|
309
|
+
console.log(`🚫 Filtering to exclude:`, excludeModels);
|
|
310
|
+
}
|
|
311
|
+
jsonlFiles = jsonlFiles.filter(f => {
|
|
312
|
+
const modelName = f.replace(/\.jsonl$/i, '');
|
|
313
|
+
const excluded = excludeLookup.has(modelName.toLowerCase());
|
|
314
|
+
if (verbose && excluded) {
|
|
315
|
+
console.log(` ⏭️ Excluding ${f}`);
|
|
316
|
+
}
|
|
317
|
+
return !excluded;
|
|
318
|
+
});
|
|
319
|
+
if (verbose) {
|
|
320
|
+
console.log(`✅ After exclusion: ${jsonlFiles.length} files:`, jsonlFiles.map(f => f.replace(/\.jsonl$/i, '')));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// Create a case-insensitive lookup for available models
|
|
324
|
+
const availableModels = new Set(jsonlFiles.map(f => f.replace(/\.jsonl$/i, '')));
|
|
325
|
+
const availableModelsLowercase = new Set(Array.from(availableModels).map(name => name.toLowerCase()));
|
|
326
|
+
// Filter modelMap to only include models that have JSONL files (case-insensitive)
|
|
327
|
+
const filteredModelMap = Object.fromEntries(Object.entries(modelMap).filter(([modelName]) => {
|
|
328
|
+
const matched = availableModelsLowercase.has(modelName.toLowerCase());
|
|
329
|
+
if (verbose && !matched) {
|
|
330
|
+
console.log(` ⏭️ Skipping model ${modelName} (no corresponding JSONL file)`);
|
|
331
|
+
}
|
|
332
|
+
return matched;
|
|
333
|
+
}));
|
|
334
|
+
if (verbose) {
|
|
335
|
+
console.log(`📦 Filtered model map contains ${Object.keys(filteredModelMap).length} models:`, Object.keys(filteredModelMap));
|
|
336
|
+
}
|
|
337
|
+
// Analyze dependencies to determine correct loading order
|
|
338
|
+
const dependencyAnalysis = this.analyzeDependencies(filteredModelMap, verbose, availableModels);
|
|
339
|
+
if (verbose) {
|
|
340
|
+
console.log('\n📋 Loading order determined:', dependencyAnalysis.loadOrder.join(' → '));
|
|
341
|
+
console.log('');
|
|
342
|
+
}
|
|
195
343
|
if (jsonlFiles.length === 0) {
|
|
196
344
|
throw new Error(`No JSONL files found in dataset directory: ${datasetPath}`);
|
|
197
345
|
}
|
|
198
346
|
if (verbose) {
|
|
199
347
|
console.log(`Found ${jsonlFiles.length} JSONL files to process:`, jsonlFiles);
|
|
200
348
|
}
|
|
349
|
+
// Create a case-insensitive lookup: lowercase model name -> actual filename
|
|
350
|
+
const filenameLookup = new Map(jsonlFiles.map(filename => {
|
|
351
|
+
const modelName = filename.replace(/\.jsonl$/i, '');
|
|
352
|
+
return [modelName.toLowerCase(), filename];
|
|
353
|
+
}));
|
|
201
354
|
const results = [];
|
|
202
355
|
const loadedEntities = {};
|
|
203
356
|
// Process files in dependency order instead of arbitrary order
|
|
204
357
|
for (const modelName of dependencyAnalysis.loadOrder) {
|
|
205
|
-
|
|
206
|
-
|
|
358
|
+
// Find the actual filename (case-insensitive lookup)
|
|
359
|
+
const fileName = filenameLookup.get(modelName.toLowerCase());
|
|
360
|
+
if (!fileName) {
|
|
207
361
|
if (verbose) {
|
|
208
|
-
console.log(`📄 No JSONL file found for model: ${modelName} (expected: ${
|
|
362
|
+
console.log(`📄 No JSONL file found for model: ${modelName} (expected: ${modelName}.jsonl). Skipping...`);
|
|
209
363
|
}
|
|
210
364
|
continue;
|
|
211
365
|
}
|
|
212
|
-
const ModelClass =
|
|
366
|
+
const ModelClass = filteredModelMap[modelName];
|
|
213
367
|
if (!ModelClass) {
|
|
214
368
|
if (verbose) {
|
|
215
369
|
console.warn(`No model mapping found for: ${modelName}. Skipping...`);
|
|
@@ -217,7 +371,8 @@ class JsonlDatasetLoader {
|
|
|
217
371
|
continue;
|
|
218
372
|
}
|
|
219
373
|
const filePath = node_path_1.default.join(datasetPath, fileName);
|
|
220
|
-
const result = await this.loadJsonlFile(filePath, ModelClass, modelName, loadedEntities, validateRecords, verbose
|
|
374
|
+
const result = await this.loadJsonlFile(filePath, ModelClass, modelName, loadedEntities, validateRecords, verbose, dataSource, modelMap // Pass full modelMap for reference resolution
|
|
375
|
+
);
|
|
221
376
|
// Store successfully loaded entities for future reference resolution
|
|
222
377
|
if (result.records.length > 0) {
|
|
223
378
|
loadedEntities[modelName] = {};
|
|
@@ -236,7 +391,9 @@ class JsonlDatasetLoader {
|
|
|
236
391
|
* Load a single JSONL file and convert records using the model's fromJSON method
|
|
237
392
|
*/
|
|
238
393
|
/**
|
|
239
|
-
* Resolves simple references in format {"id": "uuid"} to full objects from loaded entities
|
|
394
|
+
* Resolves simple references in format {"id": "uuid"} to full objects from loaded entities.
|
|
395
|
+
* Only resolves compositions and sharedCompositions, NOT regular references (to avoid circular dependencies).
|
|
396
|
+
* Also handles arrays of compositions.
|
|
240
397
|
*/
|
|
241
398
|
async resolveSimpleReferences(data, ModelClass, loadedEntities, verbose = false) {
|
|
242
399
|
if (!data || typeof data !== 'object') {
|
|
@@ -249,8 +406,46 @@ class JsonlDatasetLoader {
|
|
|
249
406
|
const fieldType = Reflect.getMetadata('field:type', ModelClass.prototype, fieldName);
|
|
250
407
|
const relationshipType = Reflect.getMetadata('field:relationship:type', ModelClass.prototype, fieldName);
|
|
251
408
|
const designType = Reflect.getMetadata('design:type', ModelClass.prototype, fieldName);
|
|
252
|
-
//
|
|
253
|
-
if ((
|
|
409
|
+
// Handle arrays of compositions (not references)
|
|
410
|
+
if (Array.isArray(resolvedData[fieldName])) {
|
|
411
|
+
// Only resolve if this is a composition/sharedComposition, not a reference
|
|
412
|
+
if (relationshipType === 'composition' || relationshipType === 'sharedComposition') {
|
|
413
|
+
const resolvedArray = [];
|
|
414
|
+
for (const item of resolvedData[fieldName]) {
|
|
415
|
+
if (item && typeof item === 'object' && item.id && Object.keys(item).length === 1) {
|
|
416
|
+
// Try to resolve the composition
|
|
417
|
+
const refId = item.id;
|
|
418
|
+
let resolved = false;
|
|
419
|
+
// Try to find the model name and resolve from loadedEntities
|
|
420
|
+
const refModelName = this.getReferencedModelName(ModelClass, fieldName);
|
|
421
|
+
if (refModelName && loadedEntities[refModelName] && loadedEntities[refModelName][refId]) {
|
|
422
|
+
resolvedArray.push(loadedEntities[refModelName][refId]);
|
|
423
|
+
resolved = true;
|
|
424
|
+
if (verbose) {
|
|
425
|
+
console.log(` Resolved composition array item ${fieldName}[].id=${refId} → ${refModelName} object`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (!resolved) {
|
|
429
|
+
// Keep the ID-only reference if we can't resolve it
|
|
430
|
+
resolvedArray.push(item);
|
|
431
|
+
if (verbose && refModelName) {
|
|
432
|
+
console.log(` Warning: Could not resolve composition ${fieldName}[].id=${refId} (${refModelName} not found)`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
// Item already has full data, keep it as is
|
|
438
|
+
resolvedArray.push(item);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
resolvedData[fieldName] = resolvedArray;
|
|
442
|
+
}
|
|
443
|
+
// For regular reference arrays, leave them as-is (don't resolve to avoid cycles)
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
// Only resolve compositions and sharedCompositions, NOT regular references
|
|
447
|
+
// This prevents circular dependency issues with fromJSON
|
|
448
|
+
if ((relationshipType === 'composition' || relationshipType === 'sharedComposition') &&
|
|
254
449
|
resolvedData[fieldName] &&
|
|
255
450
|
typeof resolvedData[fieldName] === 'object' &&
|
|
256
451
|
resolvedData[fieldName].id &&
|
|
@@ -314,7 +509,315 @@ class JsonlDatasetLoader {
|
|
|
314
509
|
}
|
|
315
510
|
return resolvedData;
|
|
316
511
|
}
|
|
317
|
-
|
|
512
|
+
/**
|
|
513
|
+
* Fetches a referenced entity from the database, using an in-memory cache keyed by
|
|
514
|
+
* `ClassName:id` to eliminate redundant queries for the same record within a load run.
|
|
515
|
+
*
|
|
516
|
+
* When the entity is not found (or an error occurs), returns a minimal object
|
|
517
|
+
* `{ id: referenceId }` so that TypeORM can still extract the FK on save.
|
|
518
|
+
*/
|
|
519
|
+
async fetchReferenceFromDb(dataSource, TargetClass, referenceId, label, verbose) {
|
|
520
|
+
const cacheKey = `${TargetClass.name}:${referenceId}`;
|
|
521
|
+
if (this.dbLookupCache.has(cacheKey)) {
|
|
522
|
+
// null in the cache means a previous lookup returned nothing — return normalized stub.
|
|
523
|
+
return this.dbLookupCache.get(cacheKey) ?? { id: referenceId };
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
const loaded = await dataSource.findOneBy(TargetClass, { id: referenceId });
|
|
527
|
+
this.dbLookupCache.set(cacheKey, loaded ?? null);
|
|
528
|
+
if (loaded) {
|
|
529
|
+
if (verbose) {
|
|
530
|
+
console.log(` ↳ Loaded ${label}="${referenceId}" from database (${TargetClass.name})`);
|
|
531
|
+
}
|
|
532
|
+
return loaded;
|
|
533
|
+
}
|
|
534
|
+
if (verbose) {
|
|
535
|
+
console.log(` ⚠️ Could not resolve ${label}="${referenceId}" (not found in database)`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
this.dbLookupCache.set(cacheKey, null);
|
|
540
|
+
if (verbose) {
|
|
541
|
+
console.log(` ⚠️ Error loading ${label}="${referenceId}" from database:`, error.message);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return { id: referenceId };
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Resolves unresolved references from the database before saving.
|
|
548
|
+
* This handles references that couldn't be resolved from loadedEntities (excluded models).
|
|
549
|
+
* For single references, sets the FK column directly for efficiency.
|
|
550
|
+
* For array references, loads entities from DB to populate the relationship.
|
|
551
|
+
*/
|
|
552
|
+
async resolveUnresolvedReferencesFromDb(modelInstance, dataSource, verbose = false) {
|
|
553
|
+
const proto = Object.getPrototypeOf(modelInstance);
|
|
554
|
+
const ModelClass = modelInstance.constructor;
|
|
555
|
+
const modelFields = Reflect.getMetadata?.('model:fields', ModelClass) || [];
|
|
556
|
+
const instanceProps = Object.getOwnPropertyNames(modelInstance);
|
|
557
|
+
const propertyNames = [...new Set([...instanceProps, ...modelFields])];
|
|
558
|
+
for (const propertyName of propertyNames) {
|
|
559
|
+
if (propertyName === 'constructor') {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
const fieldType = Reflect.getMetadata?.('field:type', modelInstance, propertyName);
|
|
564
|
+
const relationshipType = Reflect.getMetadata?.('field:relationship:type', modelInstance, propertyName);
|
|
565
|
+
// Only process reference fields
|
|
566
|
+
if (fieldType !== 'relationship' || relationshipType !== 'reference') {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const currentValue = modelInstance[propertyName];
|
|
570
|
+
// Skip if value is already resolved (BaseDataModel instance) or truly undefined/null
|
|
571
|
+
if (currentValue === undefined || currentValue === null) {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
// Also skip if it's already a BaseDataModel instance
|
|
575
|
+
const { BaseDataModel } = await import('@slingr/framework-backend');
|
|
576
|
+
if (currentValue instanceof BaseDataModel) {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
// Get the referenced model class
|
|
580
|
+
const fieldOptions = Reflect.getMetadata?.('field:type:options', proto, propertyName);
|
|
581
|
+
if (!fieldOptions?.elementType || typeof fieldOptions.elementType !== 'function') {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
const TargetClass = fieldOptions.elementType();
|
|
585
|
+
if (!TargetClass) {
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
// Handle array references
|
|
589
|
+
if (Array.isArray(currentValue)) {
|
|
590
|
+
const resolvedArray = [];
|
|
591
|
+
for (const item of currentValue) {
|
|
592
|
+
let referenceId;
|
|
593
|
+
if (typeof item === 'string') {
|
|
594
|
+
referenceId = item;
|
|
595
|
+
}
|
|
596
|
+
else if (item && typeof item === 'object' && 'id' in item) {
|
|
597
|
+
referenceId = item.id;
|
|
598
|
+
}
|
|
599
|
+
if (!referenceId) {
|
|
600
|
+
resolvedArray.push(item);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
const entity = await this.fetchReferenceFromDb(dataSource, TargetClass, referenceId, `${propertyName}[${resolvedArray.length}]`, verbose);
|
|
604
|
+
resolvedArray.push(entity);
|
|
605
|
+
}
|
|
606
|
+
modelInstance[propertyName] = resolvedArray;
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
let referenceId;
|
|
610
|
+
if (typeof currentValue === 'string') {
|
|
611
|
+
referenceId = currentValue;
|
|
612
|
+
}
|
|
613
|
+
else if (currentValue && typeof currentValue === 'object' && 'id' in currentValue) {
|
|
614
|
+
referenceId = currentValue.id;
|
|
615
|
+
}
|
|
616
|
+
if (referenceId) {
|
|
617
|
+
modelInstance[propertyName] = await this.fetchReferenceFromDb(dataSource, TargetClass, referenceId, propertyName, verbose);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
// Ignore errors for individual properties
|
|
623
|
+
if (verbose) {
|
|
624
|
+
console.log(` ⚠️ Error resolving ${propertyName}:`, error.message);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Resolves string UUID reference fields on a model instance to their loaded BaseDataModel entities.
|
|
631
|
+
*
|
|
632
|
+
* During dataset loading, reference fields are stored as plain UUID strings in JSONL files.
|
|
633
|
+
* After fromJSON(), these remain as strings. However, filter validation (ValidateRelationshipFilter)
|
|
634
|
+
* requires actual BaseDataModel instances to properly evaluate filter criteria like:
|
|
635
|
+
* filter: (company) => ({ state: company.state })
|
|
636
|
+
*
|
|
637
|
+
* Without this resolution, company.state would be a plain string, and the filter validator
|
|
638
|
+
* wouldn't be able to check if the county's state matches. This mirrors what
|
|
639
|
+
* preprocessReferenceFields does in the GraphQL/UI path (fetching from DB), but uses
|
|
640
|
+
* already-loaded entities from the dataset instead.
|
|
641
|
+
*
|
|
642
|
+
* When dataSource is provided, missing array references will be loaded from the database.
|
|
643
|
+
* This is necessary for validation when referenced entities were loaded in a previous session.
|
|
644
|
+
*
|
|
645
|
+
* @param modelMap Full map of all model classes (not filtered) - needed to look up referenced model classes
|
|
646
|
+
*/
|
|
647
|
+
async resolveReferenceFieldsToEntities(modelInstance, ModelClass, loadedEntities, verbose = false, dataSource, modelMap) {
|
|
648
|
+
const proto = Object.getPrototypeOf(modelInstance);
|
|
649
|
+
const modelFields = Reflect.getMetadata?.('model:fields', ModelClass) || [];
|
|
650
|
+
const instanceProps = Object.getOwnPropertyNames(modelInstance);
|
|
651
|
+
const propertyNames = [...new Set([...instanceProps, ...modelFields])];
|
|
652
|
+
for (const propertyName of propertyNames) {
|
|
653
|
+
if (propertyName === 'constructor') {
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
try {
|
|
657
|
+
const fieldType = Reflect.getMetadata?.('field:type', modelInstance, propertyName);
|
|
658
|
+
const relationshipType = Reflect.getMetadata?.('field:relationship:type', modelInstance, propertyName);
|
|
659
|
+
// Only process reference fields (not compositions, which are handled differently)
|
|
660
|
+
if (fieldType !== 'relationship' || relationshipType !== 'reference') {
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
const currentValue = modelInstance[propertyName];
|
|
664
|
+
// Check if the value is an array (for many-to-many or one-to-many relationships)
|
|
665
|
+
if (Array.isArray(currentValue)) {
|
|
666
|
+
// Process each item in the array
|
|
667
|
+
const resolvedArray = [];
|
|
668
|
+
let hasUnresolved = false;
|
|
669
|
+
for (const item of currentValue) {
|
|
670
|
+
// Extract ID from each item
|
|
671
|
+
let referenceId;
|
|
672
|
+
if (typeof item === 'string') {
|
|
673
|
+
referenceId = item;
|
|
674
|
+
}
|
|
675
|
+
else if (item instanceof framework_backend_1.BaseDataModel && item.id) {
|
|
676
|
+
referenceId = item.id;
|
|
677
|
+
}
|
|
678
|
+
else if (item && typeof item === 'object' && 'id' in item) {
|
|
679
|
+
referenceId = item.id;
|
|
680
|
+
}
|
|
681
|
+
if (!referenceId) {
|
|
682
|
+
resolvedArray.push(item);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
// Get elementType from field options
|
|
686
|
+
const fieldOptions = Reflect.getMetadata?.('field:type:options', proto, propertyName);
|
|
687
|
+
let targetClassName = null;
|
|
688
|
+
let TargetClass = null;
|
|
689
|
+
if (fieldOptions?.elementType && typeof fieldOptions.elementType === 'function') {
|
|
690
|
+
try {
|
|
691
|
+
const targetClass = fieldOptions.elementType();
|
|
692
|
+
if (targetClass?.name) {
|
|
693
|
+
targetClassName = targetClass.name;
|
|
694
|
+
TargetClass = targetClass;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
// Ignore - elementType() might fail if model not imported in current context
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
// If elementType() failed or wasn't available, try to get class from modelMap
|
|
702
|
+
if (!TargetClass && targetClassName && modelMap) {
|
|
703
|
+
TargetClass = modelMap[targetClassName];
|
|
704
|
+
if (verbose && TargetClass) {
|
|
705
|
+
console.log(` ↳ Resolved ${targetClassName} class from modelMap for database query`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (!targetClassName) {
|
|
709
|
+
resolvedArray.push(item);
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
// Try to resolve from loadedEntities
|
|
713
|
+
const entityMap = loadedEntities[targetClassName];
|
|
714
|
+
if (entityMap && entityMap[referenceId]) {
|
|
715
|
+
resolvedArray.push(entityMap[referenceId]);
|
|
716
|
+
}
|
|
717
|
+
else if (dataSource && TargetClass) {
|
|
718
|
+
const entity = await this.fetchReferenceFromDb(dataSource, TargetClass, referenceId, `${propertyName}[${resolvedArray.length}]`, verbose);
|
|
719
|
+
resolvedArray.push(entity);
|
|
720
|
+
if (!(entity instanceof framework_backend_1.BaseDataModel)) {
|
|
721
|
+
hasUnresolved = true;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
// No dataSource — normalize to a minimal stub so TypeORM can derive the FK on save.
|
|
726
|
+
resolvedArray.push({ id: referenceId });
|
|
727
|
+
hasUnresolved = true;
|
|
728
|
+
if (verbose) {
|
|
729
|
+
console.log(` ⚠️ Could not resolve array reference ${propertyName}[${resolvedArray.length - 1}]="${referenceId}" (${targetClassName} not found in loaded entities, no dataSource available)`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
// Always set the array - either fully resolved or with plain objects containing IDs
|
|
734
|
+
// Unresolved items will be loaded from the database before saving
|
|
735
|
+
modelInstance[propertyName] = resolvedArray;
|
|
736
|
+
if (hasUnresolved && verbose) {
|
|
737
|
+
console.log(` → Array ${propertyName} contains unresolved references - will be loaded from database before saving`);
|
|
738
|
+
}
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
// Handle single reference (non-array)
|
|
742
|
+
// Extract the ID to look up from loadedEntities.
|
|
743
|
+
// Handles three formats:
|
|
744
|
+
// 1. Plain string UUID: "554aef99-..."
|
|
745
|
+
// 2. Partial BaseDataModel: State { id: "554aef99-...", name: undefined }
|
|
746
|
+
// 3. Object with id: { id: "554aef99-..." }
|
|
747
|
+
let referenceId;
|
|
748
|
+
if (typeof currentValue === 'string') {
|
|
749
|
+
referenceId = currentValue;
|
|
750
|
+
}
|
|
751
|
+
else if (currentValue instanceof framework_backend_1.BaseDataModel && currentValue.id) {
|
|
752
|
+
referenceId = currentValue.id;
|
|
753
|
+
}
|
|
754
|
+
else if (currentValue && typeof currentValue === 'object' && 'id' in currentValue) {
|
|
755
|
+
referenceId = currentValue.id;
|
|
756
|
+
}
|
|
757
|
+
if (!referenceId) {
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
// Determine the referenced model class
|
|
761
|
+
let targetClassName = null;
|
|
762
|
+
let TargetClass = null;
|
|
763
|
+
// Try elementType from field options (most reliable for Slingr framework)
|
|
764
|
+
const fieldOptions = Reflect.getMetadata?.('field:type:options', proto, propertyName);
|
|
765
|
+
if (fieldOptions?.elementType && typeof fieldOptions.elementType === 'function') {
|
|
766
|
+
try {
|
|
767
|
+
const targetClass = fieldOptions.elementType();
|
|
768
|
+
if (targetClass?.name) {
|
|
769
|
+
targetClassName = targetClass.name;
|
|
770
|
+
TargetClass = targetClass;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
// Ignore elementType resolution errors
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
// Fallback to design:type
|
|
778
|
+
if (!targetClassName) {
|
|
779
|
+
const designType = Reflect.getMetadata?.('design:type', modelInstance, propertyName);
|
|
780
|
+
if (designType && designType.name && designType.name !== 'Array' && designType.name !== 'String') {
|
|
781
|
+
targetClassName = designType.name;
|
|
782
|
+
TargetClass = designType;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// If elementType/designType didn't give us a class, try modelMap
|
|
786
|
+
if (!TargetClass && targetClassName && modelMap) {
|
|
787
|
+
TargetClass = modelMap[targetClassName];
|
|
788
|
+
if (verbose && TargetClass) {
|
|
789
|
+
console.log(` ↳ Resolved ${targetClassName} class from modelMap for database query`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (!targetClassName) {
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
// Look up the entity in loadedEntities
|
|
796
|
+
const entityMap = loadedEntities[targetClassName];
|
|
797
|
+
if (entityMap && entityMap[referenceId]) {
|
|
798
|
+
modelInstance[propertyName] = entityMap[referenceId];
|
|
799
|
+
if (verbose) {
|
|
800
|
+
console.log(` ↳ Resolved reference ${propertyName}="${referenceId}" → ${targetClassName} instance`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
else if (dataSource && TargetClass) {
|
|
804
|
+
modelInstance[propertyName] = await this.fetchReferenceFromDb(dataSource, TargetClass, referenceId, propertyName, verbose);
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
// No dataSource — normalize to a minimal stub so TypeORM can derive the FK on save.
|
|
808
|
+
// Setting the property to a plain string UUID would be silently ignored by TypeORM.
|
|
809
|
+
modelInstance[propertyName] = { id: referenceId };
|
|
810
|
+
if (verbose) {
|
|
811
|
+
console.log(` ⚠️ Could not resolve reference ${propertyName}="${referenceId}" (${targetClassName} not found in loaded entities, no dataSource available)`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
catch {
|
|
816
|
+
// Ignore errors for individual properties
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async loadJsonlFile(filePath, ModelClass, modelName, loadedEntities, validateRecords = true, verbose = false, dataSource, modelMap) {
|
|
318
821
|
if (verbose) {
|
|
319
822
|
console.log(`Loading JSONL file: ${filePath} for model: ${modelName}`);
|
|
320
823
|
}
|
|
@@ -326,6 +829,9 @@ class JsonlDatasetLoader {
|
|
|
326
829
|
successCount: 0,
|
|
327
830
|
errorCount: 0,
|
|
328
831
|
errors: [],
|
|
832
|
+
saveSuccessCount: 0,
|
|
833
|
+
saveErrorCount: 0,
|
|
834
|
+
saveErrors: [],
|
|
329
835
|
};
|
|
330
836
|
for (let i = 0; i < lines.length; i++) {
|
|
331
837
|
try {
|
|
@@ -337,6 +843,24 @@ class JsonlDatasetLoader {
|
|
|
337
843
|
const resolvedData = await this.resolveSimpleReferences(rawData, ModelClass, loadedEntities, verbose);
|
|
338
844
|
// Use the model's fromJSON method to create the instance
|
|
339
845
|
const modelInstance = ModelClass.fromJSON(resolvedData);
|
|
846
|
+
// Restore fields that were excluded by class-transformer (e.g., fields with available: false)
|
|
847
|
+
// During dataset loading, we want to include ALL fields from the JSONL file,
|
|
848
|
+
// even those marked as excluded for API/JSON serialization
|
|
849
|
+
for (const key of Object.keys(resolvedData)) {
|
|
850
|
+
// If the field exists in resolvedData but is undefined/missing in modelInstance,
|
|
851
|
+
// it was likely excluded by @Exclude() or similar decorators
|
|
852
|
+
if (resolvedData[key] !== undefined && modelInstance[key] === undefined) {
|
|
853
|
+
modelInstance[key] = resolvedData[key];
|
|
854
|
+
if (verbose) {
|
|
855
|
+
console.log(` ↳ Restored excluded field '${key}' for ${ModelClass.name}`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
// Resolve string UUID reference fields to loaded BaseDataModel entities.
|
|
860
|
+
// This is needed for filter validation: filters like { state: company.state }
|
|
861
|
+
// need company.state to be a BaseDataModel instance (not a plain UUID string)
|
|
862
|
+
// so the validator can properly check referenced objects against filter criteria.
|
|
863
|
+
await this.resolveReferenceFieldsToEntities(modelInstance, ModelClass, loadedEntities, verbose, dataSource, modelMap);
|
|
340
864
|
// Validate the instance if requested
|
|
341
865
|
if (validateRecords) {
|
|
342
866
|
const validationErrors = await modelInstance.validate();
|
|
@@ -411,6 +935,30 @@ class JsonlDatasetLoader {
|
|
|
411
935
|
camelToSnakeCase(str) {
|
|
412
936
|
return str.replace(/([A-Z])/g, '_$1').toLowerCase();
|
|
413
937
|
}
|
|
938
|
+
/**
|
|
939
|
+
* Recursively format validation error constraints for display
|
|
940
|
+
*/
|
|
941
|
+
formatValidationConstraints(validationErrors, prefix = '', isRoot = true) {
|
|
942
|
+
const lines = [];
|
|
943
|
+
for (const error of validationErrors) {
|
|
944
|
+
const propertyPath = prefix ? `${prefix}.${error.property}` : error.property;
|
|
945
|
+
// Add constraints for this property if they exist
|
|
946
|
+
if (error.constraints) {
|
|
947
|
+
const constraintEntries = Object.entries(error.constraints);
|
|
948
|
+
if (constraintEntries.length > 0) {
|
|
949
|
+
lines.push(`${propertyPath}: ${JSON.stringify(error.constraints)}`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
// Recursively process children
|
|
953
|
+
if (error.children && error.children.length > 0) {
|
|
954
|
+
const childLines = this.formatValidationConstraints(error.children, propertyPath, false);
|
|
955
|
+
if (childLines) {
|
|
956
|
+
lines.push(childLines);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return lines.join('\n');
|
|
961
|
+
}
|
|
414
962
|
/**
|
|
415
963
|
* Load dataset into database using TypeORM with dynamic table creation
|
|
416
964
|
* This method creates tables dynamically and uses TypeORM for data insertion
|
|
@@ -421,12 +969,26 @@ class JsonlDatasetLoader {
|
|
|
421
969
|
if (!typeormDataSource || !typeormDataSource.isInitialized) {
|
|
422
970
|
throw new Error('TypeORM DataSource is not initialized');
|
|
423
971
|
}
|
|
972
|
+
// If verbose, enable logging. If not verbose, disable logging to improve performance
|
|
973
|
+
if (verbose) {
|
|
974
|
+
typeormDataSource.setOptions({ logging: ['error', 'warn', 'info'] });
|
|
975
|
+
}
|
|
976
|
+
else {
|
|
977
|
+
typeormDataSource.setOptions({ logging: false });
|
|
978
|
+
}
|
|
424
979
|
// Process results in the order they were provided (which should be dependency order)
|
|
425
980
|
for (const result of results) {
|
|
426
|
-
if (result.errorCount > 0
|
|
981
|
+
if (result.errorCount > 0) {
|
|
427
982
|
console.log(`⚠️ Model ${result.modelName} has ${result.errorCount} validation errors:`);
|
|
428
983
|
for (const error of result.errors) {
|
|
429
|
-
|
|
984
|
+
const formattedErrors = this.formatValidationConstraints(error.validationErrors);
|
|
985
|
+
console.log(` Record ${error.recordIndex + 1}:`);
|
|
986
|
+
if (formattedErrors) {
|
|
987
|
+
console.log(` ${formattedErrors.replace(/\n/g, '\n ')}`);
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
console.log(` (no constraint details available)`);
|
|
991
|
+
}
|
|
430
992
|
}
|
|
431
993
|
}
|
|
432
994
|
if (result.successCount === 0) {
|
|
@@ -438,18 +1000,58 @@ class JsonlDatasetLoader {
|
|
|
438
1000
|
if (verbose) {
|
|
439
1001
|
console.log(`📊 Processing ${result.modelName}: ${result.successCount} valid records`);
|
|
440
1002
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
1003
|
+
// Create progress bar for this model
|
|
1004
|
+
const progressBar = new cliProgress.SingleBar({
|
|
1005
|
+
format: ` Loading ${result.modelName} [{bar}] {percentage}% | {value}/{total} records | Success: {success} | Failed: {failed}`,
|
|
1006
|
+
barCompleteChar: '█',
|
|
1007
|
+
barIncompleteChar: '░',
|
|
1008
|
+
hideCursor: true,
|
|
1009
|
+
});
|
|
1010
|
+
progressBar.start(result.records.length, 0, { success: 0, failed: 0 });
|
|
1011
|
+
// Save records using the framework's save method which respects relationships
|
|
1012
|
+
// Continue on errors instead of stopping
|
|
1013
|
+
for (let i = 0; i < result.records.length; i++) {
|
|
1014
|
+
const record = result.records[i];
|
|
1015
|
+
try {
|
|
1016
|
+
// Before saving, resolve any unresolved references from the database
|
|
1017
|
+
// This handles cases where referenced models were excluded from loading but exist in DB
|
|
1018
|
+
await this.resolveUnresolvedReferencesFromDb(record, dataSource, verbose);
|
|
444
1019
|
await dataSource.save(record);
|
|
1020
|
+
result.saveSuccessCount++;
|
|
445
1021
|
}
|
|
446
|
-
|
|
447
|
-
|
|
1022
|
+
catch (error) {
|
|
1023
|
+
result.saveErrorCount++;
|
|
1024
|
+
result.saveErrors.push({
|
|
1025
|
+
recordIndex: i,
|
|
1026
|
+
record: record,
|
|
1027
|
+
error: error.message,
|
|
1028
|
+
});
|
|
448
1029
|
}
|
|
1030
|
+
// Update progress bar
|
|
1031
|
+
progressBar.update(i + 1, {
|
|
1032
|
+
success: result.saveSuccessCount,
|
|
1033
|
+
failed: result.saveErrorCount,
|
|
1034
|
+
});
|
|
449
1035
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
1036
|
+
progressBar.stop();
|
|
1037
|
+
// Show summary for this model
|
|
1038
|
+
if (result.saveSuccessCount > 0) {
|
|
1039
|
+
console.log(` ✅ Successfully saved ${result.saveSuccessCount} records`);
|
|
1040
|
+
}
|
|
1041
|
+
if (result.saveErrorCount > 0) {
|
|
1042
|
+
console.log(` ❌ Failed to save ${result.saveErrorCount} records`);
|
|
1043
|
+
// Show first few errors if verbose
|
|
1044
|
+
if (verbose && result.saveErrors.length > 0) {
|
|
1045
|
+
const errorsToShow = Math.min(3, result.saveErrors.length);
|
|
1046
|
+
console.log(` First ${errorsToShow} errors:`);
|
|
1047
|
+
for (let i = 0; i < errorsToShow; i++) {
|
|
1048
|
+
const saveError = result.saveErrors[i];
|
|
1049
|
+
console.log(` Record ${saveError.recordIndex + 1}: ${saveError.error}`);
|
|
1050
|
+
}
|
|
1051
|
+
if (result.saveErrors.length > errorsToShow) {
|
|
1052
|
+
console.log(` ... and ${result.saveErrors.length - errorsToShow} more errors`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
453
1055
|
}
|
|
454
1056
|
}
|
|
455
1057
|
}
|
|
@@ -552,15 +1154,20 @@ exports.JsonlDatasetLoader = JsonlDatasetLoader;
|
|
|
552
1154
|
/**
|
|
553
1155
|
* Auto-discover models from the compiled JavaScript files
|
|
554
1156
|
*/
|
|
555
|
-
async function discoverModels(distPath) {
|
|
1157
|
+
async function discoverModels(distPath, verbose = false) {
|
|
556
1158
|
const modelMap = {};
|
|
557
1159
|
if (!(await fs_extra_1.default.pathExists(distPath))) {
|
|
558
1160
|
throw new Error(`Dist directory not found: ${distPath}. Please run 'npm run build' first.`);
|
|
559
1161
|
}
|
|
560
1162
|
// Import glob dynamically since it might be ESM
|
|
561
1163
|
const { glob } = await import('glob');
|
|
562
|
-
//
|
|
563
|
-
|
|
1164
|
+
// Check both dist/data (legacy) and dist/src/data (current TypeScript output)
|
|
1165
|
+
// Try primary location first
|
|
1166
|
+
let entityFiles = await glob(node_path_1.default.join(distPath, 'src', 'data', '**', '*.js').replace(/\\/g, '/')).then((files) => files.filter((file) => !file.includes('.test.js')));
|
|
1167
|
+
// If no files found, try alternative path (dist/src/data)
|
|
1168
|
+
if (entityFiles.length === 0) {
|
|
1169
|
+
entityFiles = await glob(node_path_1.default.join(distPath, 'data', '**', '*.js').replace(/\\/g, '/')).then((files) => files.filter((file) => !file.includes('.test.js')));
|
|
1170
|
+
}
|
|
564
1171
|
for (const file of entityFiles) {
|
|
565
1172
|
try {
|
|
566
1173
|
// Clear the require cache to ensure fresh loading
|
|
@@ -569,13 +1176,17 @@ async function discoverModels(distPath) {
|
|
|
569
1176
|
const exports = Object.values(module);
|
|
570
1177
|
for (const exp of exports) {
|
|
571
1178
|
if (typeof exp === 'function' && exp.prototype && exp.name && typeof exp.fromJSON === 'function') {
|
|
572
|
-
|
|
1179
|
+
if (verbose) {
|
|
1180
|
+
console.log(`✅ Found model: ${exp.name} in ${file}`);
|
|
1181
|
+
}
|
|
573
1182
|
modelMap[exp.name] = exp;
|
|
574
1183
|
}
|
|
575
1184
|
}
|
|
576
1185
|
}
|
|
577
1186
|
catch (error) {
|
|
578
|
-
|
|
1187
|
+
if (verbose) {
|
|
1188
|
+
console.warn(`Failed to load entity from file: ${file}:`, error);
|
|
1189
|
+
}
|
|
579
1190
|
}
|
|
580
1191
|
}
|
|
581
1192
|
return modelMap;
|