@qelos/plugins-cli 0.0.15 → 0.0.17
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/commands/blueprints.mjs
CHANGED
|
@@ -17,6 +17,12 @@ export default function blueprintsCommand(program) {
|
|
|
17
17
|
required: false,
|
|
18
18
|
default: 'mongodb://localhost:27017/db'
|
|
19
19
|
})
|
|
20
|
+
.option('guides', {
|
|
21
|
+
describe: 'Generate SDK guides for each blueprint',
|
|
22
|
+
type: 'boolean',
|
|
23
|
+
required: false,
|
|
24
|
+
default: true
|
|
25
|
+
})
|
|
20
26
|
},
|
|
21
27
|
blueprintsController)
|
|
22
28
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
|
-
import { MongoClient } from "mongodb";
|
|
4
2
|
import { logger } from "../services/logger.mjs";
|
|
3
|
+
import {
|
|
4
|
+
SUPPORTED_PROTOCOL,
|
|
5
|
+
generateBlueprintsFromMongo,
|
|
6
|
+
} from "../services/blueprint-generator.mjs";
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
const SAMPLE_SIZE = 50;
|
|
8
|
-
|
|
9
|
-
export default async function blueprintsController({ uri, path: targetPath }) {
|
|
8
|
+
export default async function blueprintsController({ uri, path: targetPath, guides = true }) {
|
|
10
9
|
const connectionUri = uri || "mongodb://localhost:27017/db";
|
|
11
10
|
|
|
12
11
|
if (!SUPPORTED_PROTOCOL.test(connectionUri)) {
|
|
@@ -15,322 +14,15 @@ export default async function blueprintsController({ uri, path: targetPath }) {
|
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
const targetDir = path.join(process.cwd(), targetPath);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const client = new MongoClient(connectionUri, {
|
|
21
|
-
serverSelectionTimeoutMS: 10_000,
|
|
22
|
-
});
|
|
17
|
+
const shouldGenerateGuides = guides !== false;
|
|
23
18
|
|
|
24
19
|
try {
|
|
25
|
-
await
|
|
26
|
-
|
|
27
|
-
const db = client.db(dbName);
|
|
28
|
-
|
|
29
|
-
logger.section(`Connected to MongoDB database: ${db.databaseName}`);
|
|
30
|
-
|
|
31
|
-
const collections = await db.listCollections().toArray();
|
|
32
|
-
const filteredCollections = collections.filter(
|
|
33
|
-
(collection) => !collection.name.startsWith("system.")
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
if (filteredCollections.length === 0) {
|
|
37
|
-
logger.warning("No collections found to generate blueprints from.");
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
for (const collection of filteredCollections) {
|
|
42
|
-
await generateBlueprintForCollection(db, collection.name, targetDir);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
logger.success(
|
|
46
|
-
`Generated ${filteredCollections.length} blueprint file(s) in ${targetDir}`
|
|
47
|
-
);
|
|
48
|
-
} catch (error) {
|
|
49
|
-
logger.error("Failed to generate blueprints", error);
|
|
50
|
-
process.exit(1);
|
|
51
|
-
} finally {
|
|
52
|
-
await client.close().catch(() => {});
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function ensureDirectory(targetDir) {
|
|
57
|
-
if (!fs.existsSync(targetDir)) {
|
|
58
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
59
|
-
logger.info(`Created output directory at ${targetDir}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function generateBlueprintForCollection(db, collectionName, targetDir) {
|
|
64
|
-
logger.step(`Analyzing collection: ${collectionName}`);
|
|
65
|
-
try {
|
|
66
|
-
const collection = db.collection(collectionName);
|
|
67
|
-
const documents = await sampleCollectionDocuments(collection);
|
|
68
|
-
const properties = buildProperties(collectionName, documents);
|
|
69
|
-
|
|
70
|
-
const blueprint = createBlueprintPayload(collectionName, properties);
|
|
71
|
-
const filePath = path.join(
|
|
20
|
+
await generateBlueprintsFromMongo({
|
|
21
|
+
uri: connectionUri,
|
|
72
22
|
targetDir,
|
|
73
|
-
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
fs.writeFileSync(filePath, JSON.stringify(blueprint, null, 2));
|
|
77
|
-
logger.success(`Blueprint generated: ${filePath}`);
|
|
78
|
-
} catch (error) {
|
|
79
|
-
logger.error(`Failed to process collection ${collectionName}`, error);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function buildProperties(collectionName, documents) {
|
|
84
|
-
const properties = {};
|
|
85
|
-
let processedDocuments = 0;
|
|
86
|
-
|
|
87
|
-
for (const doc of documents) {
|
|
88
|
-
if (processedDocuments >= SAMPLE_SIZE) {
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
processedDocuments += 1;
|
|
92
|
-
if (!doc || typeof doc !== "object") continue;
|
|
93
|
-
|
|
94
|
-
for (const [key, value] of Object.entries(doc)) {
|
|
95
|
-
if (shouldSkipField(key) || properties[key]) continue;
|
|
96
|
-
properties[key] = createPropertyDescriptor(key, value, collectionName);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (Object.keys(properties).length === 0) {
|
|
101
|
-
logger.warning(
|
|
102
|
-
`No properties detected for collection ${collectionName}. Generated blueprint will contain empty properties.`
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return properties;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function createPropertyDescriptor(key, sampleValue, collectionName) {
|
|
110
|
-
const { normalizedValue, multi } = normalizeSampleValue(sampleValue);
|
|
111
|
-
const type = detectBlueprintType(normalizedValue);
|
|
112
|
-
|
|
113
|
-
const descriptor = {
|
|
114
|
-
title: formatTitle(key),
|
|
115
|
-
type,
|
|
116
|
-
description: "",
|
|
117
|
-
required: false,
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
if (multi) {
|
|
121
|
-
descriptor.multi = true;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (type === "object") {
|
|
125
|
-
descriptor.schema = buildObjectSchema(normalizedValue);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return descriptor;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function normalizeSampleValue(value) {
|
|
132
|
-
if (Array.isArray(value)) {
|
|
133
|
-
const firstValue = value.find(
|
|
134
|
-
(item) => item !== null && item !== undefined
|
|
135
|
-
);
|
|
136
|
-
return { normalizedValue: firstValue ?? null, multi: true };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return { normalizedValue: value, multi: false };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function detectBlueprintType(value) {
|
|
143
|
-
if (value === null || value === undefined) {
|
|
144
|
-
return "string";
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (value instanceof Date) {
|
|
148
|
-
return "datetime";
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (typeof value === "number") {
|
|
152
|
-
return "number";
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (typeof value === "boolean") {
|
|
156
|
-
return "boolean";
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (typeof value === "object") {
|
|
160
|
-
if (value?._bsontype === "ObjectId") {
|
|
161
|
-
return "string";
|
|
162
|
-
}
|
|
163
|
-
return "object";
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return "string";
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function createBlueprintPayload(collectionName, properties) {
|
|
170
|
-
const singularName = ensureSingular(collectionName);
|
|
171
|
-
return {
|
|
172
|
-
identifier: toIdentifier(singularName),
|
|
173
|
-
name: formatTitle(singularName),
|
|
174
|
-
description: `Auto-generated blueprint for MongoDB collection "${collectionName}"`,
|
|
175
|
-
entityIdentifierMechanism: "objectid",
|
|
176
|
-
permissions: createDefaultPermissions(),
|
|
177
|
-
permissionScope: "workspace",
|
|
178
|
-
properties,
|
|
179
|
-
relations: [],
|
|
180
|
-
dispatchers: {
|
|
181
|
-
create: false,
|
|
182
|
-
update: false,
|
|
183
|
-
delete: false,
|
|
184
|
-
},
|
|
185
|
-
limitations: [],
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function createDefaultPermissions() {
|
|
190
|
-
const operations = ["create", "read", "update", "delete"];
|
|
191
|
-
return operations.map((operation) => ({
|
|
192
|
-
scope: "workspace",
|
|
193
|
-
operation,
|
|
194
|
-
guest: false,
|
|
195
|
-
roleBased: ["*"],
|
|
196
|
-
workspaceRoleBased: ["*"],
|
|
197
|
-
workspaceLabelsBased: ["*"],
|
|
198
|
-
}));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function toIdentifier(collectionName) {
|
|
202
|
-
const sanitized = collectionName
|
|
203
|
-
.trim()
|
|
204
|
-
.toLowerCase()
|
|
205
|
-
.replace(/[^a-z0-9]+/g, "_")
|
|
206
|
-
.replace(/^_+|_+$/g, "");
|
|
207
|
-
|
|
208
|
-
return sanitized || `collection_${Date.now()}`;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function formatTitle(value) {
|
|
212
|
-
return value
|
|
213
|
-
.replace(/[_-]+/g, " ")
|
|
214
|
-
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
215
|
-
.replace(/\s+/g, " ")
|
|
216
|
-
.trim()
|
|
217
|
-
.replace(/\b\w/g, (match) => match.toUpperCase());
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
async function sampleCollectionDocuments(collection) {
|
|
221
|
-
try {
|
|
222
|
-
return await collection
|
|
223
|
-
.aggregate([{ $sample: { size: SAMPLE_SIZE } }])
|
|
224
|
-
.toArray();
|
|
225
|
-
} catch (error) {
|
|
226
|
-
logger.debug(
|
|
227
|
-
`Falling back to sequential sampling for ${collection.collectionName}: ${error.message}`
|
|
228
|
-
);
|
|
229
|
-
return collection.find({}).limit(SAMPLE_SIZE).toArray();
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function buildObjectSchema(sample, depth = 0) {
|
|
234
|
-
const MAX_SCHEMA_DEPTH = 3;
|
|
235
|
-
if (!sample || typeof sample !== "object" || depth >= MAX_SCHEMA_DEPTH) {
|
|
236
|
-
return { type: "object" };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const properties = {};
|
|
240
|
-
|
|
241
|
-
for (const [key, value] of Object.entries(sample)) {
|
|
242
|
-
if (key === "_id" || value === undefined) continue;
|
|
243
|
-
properties[key] = buildSchemaFromValue(value, depth + 1);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (Object.keys(properties).length === 0) {
|
|
247
|
-
return { type: "object" };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return {
|
|
251
|
-
type: "object",
|
|
252
|
-
properties,
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function buildSchemaFromValue(value, depth) {
|
|
257
|
-
if (Array.isArray(value)) {
|
|
258
|
-
const arraySample = value.find(
|
|
259
|
-
(item) => item !== null && item !== undefined
|
|
260
|
-
);
|
|
261
|
-
const itemsSchema = arraySample
|
|
262
|
-
? buildSchemaFromValue(arraySample, depth + 1)
|
|
263
|
-
: { type: "string" };
|
|
264
|
-
return {
|
|
265
|
-
type: "array",
|
|
266
|
-
items: itemsSchema,
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const valueType = detectBlueprintType(value);
|
|
271
|
-
|
|
272
|
-
if (valueType === "object" && value && typeof value === "object") {
|
|
273
|
-
return buildObjectSchema(value, depth + 1);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return {
|
|
277
|
-
type: mapBlueprintTypeToJsonSchema(valueType),
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function mapBlueprintTypeToJsonSchema(type) {
|
|
282
|
-
switch (type) {
|
|
283
|
-
case "number":
|
|
284
|
-
return "number";
|
|
285
|
-
case "boolean":
|
|
286
|
-
return "boolean";
|
|
287
|
-
case "object":
|
|
288
|
-
return "object";
|
|
289
|
-
default:
|
|
290
|
-
return "string";
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function shouldSkipField(key) {
|
|
295
|
-
if (!key) return true;
|
|
296
|
-
const normalized = key.toLowerCase();
|
|
297
|
-
if (
|
|
298
|
-
normalized === "_id" ||
|
|
299
|
-
normalized === "id" ||
|
|
300
|
-
normalized === "user" ||
|
|
301
|
-
normalized === "userid" ||
|
|
302
|
-
normalized === "workspace" ||
|
|
303
|
-
normalized === "workspaceid"
|
|
304
|
-
) {
|
|
305
|
-
return true;
|
|
306
|
-
}
|
|
307
|
-
return key.startsWith("__");
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function ensureSingular(value = "") {
|
|
311
|
-
const normalized = value.trim();
|
|
312
|
-
const lower = normalized.toLowerCase();
|
|
313
|
-
|
|
314
|
-
if (lower.endsWith("ies")) {
|
|
315
|
-
return normalized.slice(0, -3) + normalized.slice(-3).replace(/ies$/i, "y");
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (/(sses|xes|zes|ches|shes)$/i.test(lower)) {
|
|
319
|
-
return normalized.slice(0, -2);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (lower.endsWith("s") && !lower.endsWith("ss")) {
|
|
323
|
-
return normalized.slice(0, -1);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return normalized;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function getDatabaseName(connectionUri) {
|
|
330
|
-
try {
|
|
331
|
-
const parsed = new URL(connectionUri);
|
|
332
|
-
return parsed.pathname.replace(/^\//, "") || undefined;
|
|
23
|
+
createGuides: shouldGenerateGuides,
|
|
24
|
+
});
|
|
333
25
|
} catch {
|
|
334
|
-
|
|
26
|
+
process.exit(1);
|
|
335
27
|
}
|
|
336
|
-
}
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qelos/plugins-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"description": "CLI to manage QELOS plugins",
|
|
5
5
|
"main": "cli.mjs",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"author": "David Meir-Levy <davidmeirlevy@gmail.com>",
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@qelos/sdk": "^3.11.
|
|
17
|
+
"@qelos/sdk": "^3.11.1",
|
|
18
18
|
"cli-progress": "^3.12.0",
|
|
19
19
|
"cli-select": "^1.1.2",
|
|
20
20
|
"decompress-zip": "^0.3.3",
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { MongoClient } from 'mongodb';
|
|
4
|
+
import { logger } from './logger.mjs';
|
|
5
|
+
import { generateSdkGuide } from './blueprint-sdk-guides.mjs';
|
|
6
|
+
import {
|
|
7
|
+
formatTitle,
|
|
8
|
+
toIdentifier,
|
|
9
|
+
ensureSingular,
|
|
10
|
+
detectBlueprintType,
|
|
11
|
+
mapBlueprintTypeToJsonSchema
|
|
12
|
+
} from './blueprint-shared.mjs';
|
|
13
|
+
|
|
14
|
+
export const SUPPORTED_PROTOCOL = /^mongodb:\/\//i;
|
|
15
|
+
const SAMPLE_SIZE = 50;
|
|
16
|
+
|
|
17
|
+
export async function generateBlueprintsFromMongo({ uri, targetDir, createGuides = true }) {
|
|
18
|
+
ensureDirectory(targetDir);
|
|
19
|
+
const client = new MongoClient(uri, { serverSelectionTimeoutMS: 10_000 });
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await client.connect();
|
|
23
|
+
const dbName = getDatabaseName(uri);
|
|
24
|
+
const db = client.db(dbName);
|
|
25
|
+
|
|
26
|
+
logger.section(`Connected to MongoDB database: ${db.databaseName}`);
|
|
27
|
+
|
|
28
|
+
const collections = await db.listCollections().toArray();
|
|
29
|
+
const filteredCollections = collections.filter(({ name }) => !name.startsWith('system.'));
|
|
30
|
+
|
|
31
|
+
if (filteredCollections.length === 0) {
|
|
32
|
+
logger.warning('No collections found to generate blueprints from.');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const collection of filteredCollections) {
|
|
37
|
+
await generateBlueprintForCollection({
|
|
38
|
+
db,
|
|
39
|
+
collectionName: collection.name,
|
|
40
|
+
targetDir,
|
|
41
|
+
createGuides,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
logger.success(`Generated ${filteredCollections.length} blueprint file(s) in ${targetDir}`);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
logger.error('Failed to generate blueprints', error);
|
|
48
|
+
throw error;
|
|
49
|
+
} finally {
|
|
50
|
+
await client.close().catch(() => {});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function generateBlueprintForCollection({ db, collectionName, targetDir, createGuides }) {
|
|
55
|
+
logger.step(`Analyzing collection: ${collectionName}`);
|
|
56
|
+
try {
|
|
57
|
+
const collection = db.collection(collectionName);
|
|
58
|
+
const documents = await sampleCollectionDocuments(collection);
|
|
59
|
+
const properties = buildProperties(collectionName, documents);
|
|
60
|
+
|
|
61
|
+
const blueprint = createBlueprintPayload(collectionName, properties);
|
|
62
|
+
const blueprintPath = path.join(targetDir, `${blueprint.identifier}.blueprint.json`);
|
|
63
|
+
|
|
64
|
+
fs.writeFileSync(blueprintPath, JSON.stringify(blueprint, null, 2));
|
|
65
|
+
logger.success(`Blueprint generated: ${blueprintPath}`);
|
|
66
|
+
|
|
67
|
+
if (createGuides) {
|
|
68
|
+
generateSdkGuide({ blueprint, documents, targetDir });
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.error(`Failed to process collection ${collectionName}`, error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildProperties(collectionName, documents) {
|
|
76
|
+
const properties = {};
|
|
77
|
+
let processedDocuments = 0;
|
|
78
|
+
|
|
79
|
+
for (const doc of documents) {
|
|
80
|
+
if (processedDocuments >= SAMPLE_SIZE) {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
processedDocuments += 1;
|
|
84
|
+
if (!doc || typeof doc !== 'object') continue;
|
|
85
|
+
|
|
86
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
87
|
+
if (shouldSkipField(key) || properties[key]) continue;
|
|
88
|
+
properties[key] = createPropertyDescriptor(key, value, collectionName);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (Object.keys(properties).length === 0) {
|
|
93
|
+
logger.warning(
|
|
94
|
+
`No properties detected for collection ${collectionName}. Generated blueprint will contain empty properties.`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return properties;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function createPropertyDescriptor(key, sampleValue, collectionName) {
|
|
102
|
+
const { normalizedValue, multi } = normalizeSampleValue(sampleValue);
|
|
103
|
+
const type = detectBlueprintType(normalizedValue);
|
|
104
|
+
|
|
105
|
+
const descriptor = {
|
|
106
|
+
title: formatTitle(key),
|
|
107
|
+
type,
|
|
108
|
+
description: '',
|
|
109
|
+
required: false
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (multi) {
|
|
113
|
+
descriptor.multi = true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (type === 'object') {
|
|
117
|
+
descriptor.schema = buildObjectSchema(normalizedValue);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return descriptor;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeSampleValue(value) {
|
|
124
|
+
if (Array.isArray(value)) {
|
|
125
|
+
const firstValue = value.find((item) => item !== null && item !== undefined);
|
|
126
|
+
return { normalizedValue: firstValue ?? null, multi: true };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { normalizedValue: value, multi: false };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function createBlueprintPayload(collectionName, properties) {
|
|
133
|
+
const singularName = ensureSingular(collectionName);
|
|
134
|
+
return {
|
|
135
|
+
identifier: toIdentifier(singularName),
|
|
136
|
+
name: formatTitle(singularName),
|
|
137
|
+
description: `Auto-generated blueprint for MongoDB collection "${collectionName}"`,
|
|
138
|
+
entityIdentifierMechanism: 'objectid',
|
|
139
|
+
permissions: createDefaultPermissions(),
|
|
140
|
+
permissionScope: 'workspace',
|
|
141
|
+
properties,
|
|
142
|
+
relations: [],
|
|
143
|
+
dispatchers: {
|
|
144
|
+
create: false,
|
|
145
|
+
update: false,
|
|
146
|
+
delete: false
|
|
147
|
+
},
|
|
148
|
+
limitations: []
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function createDefaultPermissions() {
|
|
153
|
+
const operations = ['create', 'read', 'update', 'delete'];
|
|
154
|
+
return operations.map((operation) => ({
|
|
155
|
+
scope: 'workspace',
|
|
156
|
+
operation,
|
|
157
|
+
guest: false,
|
|
158
|
+
roleBased: ['*'],
|
|
159
|
+
workspaceRoleBased: ['*'],
|
|
160
|
+
workspaceLabelsBased: ['*']
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function sampleCollectionDocuments(collection) {
|
|
165
|
+
try {
|
|
166
|
+
return await collection.aggregate([{ $sample: { size: SAMPLE_SIZE } }]).toArray();
|
|
167
|
+
} catch (error) {
|
|
168
|
+
logger.debug(`Falling back to sequential sampling for ${collection.collectionName}: ${error.message}`);
|
|
169
|
+
return collection.find({}).limit(SAMPLE_SIZE).toArray();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function buildObjectSchema(sample, depth = 0) {
|
|
174
|
+
const MAX_SCHEMA_DEPTH = 3;
|
|
175
|
+
if (!sample || typeof sample !== 'object' || depth >= MAX_SCHEMA_DEPTH) {
|
|
176
|
+
return { type: 'object' };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const properties = {};
|
|
180
|
+
|
|
181
|
+
for (const [key, value] of Object.entries(sample)) {
|
|
182
|
+
if (key === '_id' || value === undefined) continue;
|
|
183
|
+
properties[key] = buildSchemaFromValue(value, depth + 1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (Object.keys(properties).length === 0) {
|
|
187
|
+
return { type: 'object' };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
type: 'object',
|
|
192
|
+
properties
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function buildSchemaFromValue(value, depth) {
|
|
197
|
+
if (Array.isArray(value)) {
|
|
198
|
+
const arraySample = value.find((item) => item !== null && item !== undefined);
|
|
199
|
+
const itemsSchema = arraySample ? buildSchemaFromValue(arraySample, depth + 1) : { type: 'string' };
|
|
200
|
+
return {
|
|
201
|
+
type: 'array',
|
|
202
|
+
items: itemsSchema
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const valueType = detectBlueprintType(value);
|
|
207
|
+
|
|
208
|
+
if (valueType === 'object' && value && typeof value === 'object') {
|
|
209
|
+
return buildObjectSchema(value, depth + 1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
type: mapBlueprintTypeToJsonSchema(valueType)
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function shouldSkipField(key) {
|
|
218
|
+
if (!key) return true;
|
|
219
|
+
const normalized = key.toLowerCase();
|
|
220
|
+
if (
|
|
221
|
+
normalized === '_id' ||
|
|
222
|
+
normalized === 'id' ||
|
|
223
|
+
normalized === 'user' ||
|
|
224
|
+
normalized === 'userid' ||
|
|
225
|
+
normalized === 'workspace' ||
|
|
226
|
+
normalized === 'workspaceid'
|
|
227
|
+
) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
return key.startsWith('__');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function ensureDirectory(targetDir) {
|
|
234
|
+
if (!fs.existsSync(targetDir)) {
|
|
235
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
236
|
+
logger.info(`Created output directory at ${targetDir}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function getDatabaseName(connectionUri) {
|
|
241
|
+
try {
|
|
242
|
+
const parsed = new URL(connectionUri);
|
|
243
|
+
return parsed.pathname.replace(/^\//, '') || undefined;
|
|
244
|
+
} catch {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { formatTitle, detectBlueprintType } from './blueprint-shared.mjs';
|
|
4
|
+
|
|
5
|
+
export function generateSdkGuide({ blueprint, documents, targetDir }) {
|
|
6
|
+
const guidePath = path.join(targetDir, `${blueprint.identifier}.sdk.md`);
|
|
7
|
+
const interfaceName = `${formatTitle(blueprint.identifier).replace(/\s+/g, '')}Entity`;
|
|
8
|
+
const entityVarName = `${toCamelCase(blueprint.identifier)}Entities`;
|
|
9
|
+
const sampleDoc = documents.find((doc) => doc && typeof doc === 'object');
|
|
10
|
+
const interfaceDefinition = buildInterfaceDefinition(interfaceName, blueprint);
|
|
11
|
+
const exampleEntity = buildExampleEntity(blueprint, sampleDoc);
|
|
12
|
+
const exampleLiteral = stringifyObjectLiteral(exampleEntity);
|
|
13
|
+
|
|
14
|
+
const markdown = buildMarkdown({
|
|
15
|
+
blueprint,
|
|
16
|
+
interfaceName,
|
|
17
|
+
entityVarName,
|
|
18
|
+
interfaceDefinition,
|
|
19
|
+
exampleLiteral,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
fs.writeFileSync(guidePath, markdown);
|
|
23
|
+
return guidePath;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildMarkdown({ blueprint, interfaceName, entityVarName, interfaceDefinition, exampleLiteral }) {
|
|
27
|
+
return [
|
|
28
|
+
`# ${blueprint.name} Blueprint SDK Guide`,
|
|
29
|
+
'',
|
|
30
|
+
'## Install the SDK',
|
|
31
|
+
'```bash',
|
|
32
|
+
'npm install @qelos/sdk',
|
|
33
|
+
'```',
|
|
34
|
+
'',
|
|
35
|
+
'## Initialize the Administrator SDK',
|
|
36
|
+
'```ts',
|
|
37
|
+
"import QelosAdministratorSDK from '@qelos/sdk/administrator';",
|
|
38
|
+
'',
|
|
39
|
+
'const sdk = new QelosAdministratorSDK({',
|
|
40
|
+
" appUrl: process.env.QELOS_URL || 'http://localhost:3000',",
|
|
41
|
+
' fetch,',
|
|
42
|
+
'});',
|
|
43
|
+
'',
|
|
44
|
+
`const ${entityVarName} = sdk.blueprints.entitiesOf<${interfaceName}>('${blueprint.identifier}');`,
|
|
45
|
+
'```',
|
|
46
|
+
'',
|
|
47
|
+
'## TypeScript Interface',
|
|
48
|
+
'```ts',
|
|
49
|
+
interfaceDefinition,
|
|
50
|
+
'```',
|
|
51
|
+
'',
|
|
52
|
+
'## Example Entity Payload',
|
|
53
|
+
'```ts',
|
|
54
|
+
`const sample${interfaceName} = ${exampleLiteral};`,
|
|
55
|
+
'```',
|
|
56
|
+
'',
|
|
57
|
+
'## CRUD Examples',
|
|
58
|
+
'',
|
|
59
|
+
'### List Entities',
|
|
60
|
+
'```ts',
|
|
61
|
+
`const entities = await ${entityVarName}.getList({ $limit: 20, $sort: '-created' });`,
|
|
62
|
+
'```',
|
|
63
|
+
'',
|
|
64
|
+
'### Fetch a Single Entity',
|
|
65
|
+
'```ts',
|
|
66
|
+
`const entity = await ${entityVarName}.getEntity('replace-with-entity-id');`,
|
|
67
|
+
'```',
|
|
68
|
+
'',
|
|
69
|
+
'### Create an Entity',
|
|
70
|
+
'```ts',
|
|
71
|
+
`const created = await ${entityVarName}.create(${exampleLiteral});`,
|
|
72
|
+
'```',
|
|
73
|
+
'',
|
|
74
|
+
'### Update an Entity',
|
|
75
|
+
'```ts',
|
|
76
|
+
`const updated = await ${entityVarName}.update('replace-with-entity-id', {\n ...${exampleLiteral.replace(/\n/g, '\n ')},\n});`,
|
|
77
|
+
'```',
|
|
78
|
+
'',
|
|
79
|
+
'### Delete an Entity',
|
|
80
|
+
'```ts',
|
|
81
|
+
`await ${entityVarName}.remove('replace-with-entity-id');`,
|
|
82
|
+
'```',
|
|
83
|
+
].join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildInterfaceDefinition(interfaceName, blueprint) {
|
|
87
|
+
const lines = [`export interface ${interfaceName} {`];
|
|
88
|
+
for (const [key, descriptor] of Object.entries(blueprint.properties)) {
|
|
89
|
+
const tsType = mapBlueprintPropertyToTs(descriptor);
|
|
90
|
+
const optionalFlag = descriptor.required ? '' : '?';
|
|
91
|
+
const description = descriptor.title || key;
|
|
92
|
+
lines.push(` ${key}${optionalFlag}: ${tsType}; // ${description}`);
|
|
93
|
+
}
|
|
94
|
+
lines.push('}');
|
|
95
|
+
return lines.join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function mapBlueprintPropertyToTs(descriptor) {
|
|
99
|
+
const baseType = descriptor.type;
|
|
100
|
+
let tsType;
|
|
101
|
+
switch (baseType) {
|
|
102
|
+
case 'number':
|
|
103
|
+
tsType = 'number';
|
|
104
|
+
break;
|
|
105
|
+
case 'boolean':
|
|
106
|
+
tsType = 'boolean';
|
|
107
|
+
break;
|
|
108
|
+
case 'date':
|
|
109
|
+
case 'datetime':
|
|
110
|
+
case 'time':
|
|
111
|
+
case 'file':
|
|
112
|
+
case 'string':
|
|
113
|
+
tsType = 'string';
|
|
114
|
+
break;
|
|
115
|
+
case 'object':
|
|
116
|
+
tsType = 'Record<string, any>';
|
|
117
|
+
break;
|
|
118
|
+
default:
|
|
119
|
+
tsType = 'any';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (descriptor.multi) {
|
|
123
|
+
return `${tsType}[]`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return tsType;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildExampleEntity(blueprint, sampleDoc = {}) {
|
|
130
|
+
const entity = {};
|
|
131
|
+
for (const [key, descriptor] of Object.entries(blueprint.properties)) {
|
|
132
|
+
if (sampleDoc && sampleDoc[key] !== undefined) {
|
|
133
|
+
entity[key] = sanitizeExampleValue(sampleDoc[key], descriptor);
|
|
134
|
+
} else {
|
|
135
|
+
entity[key] = getDefaultValueForDescriptor(descriptor, key);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return entity;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function sanitizeExampleValue(value, descriptor) {
|
|
142
|
+
if (Array.isArray(value)) {
|
|
143
|
+
const normalizedItems = value
|
|
144
|
+
.filter((item) => item !== null && item !== undefined)
|
|
145
|
+
.map((item) => sanitizeExampleValue(item, { ...descriptor, multi: false }));
|
|
146
|
+
return descriptor.multi ? normalizedItems : normalizedItems[0];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (value && typeof value === 'object') {
|
|
150
|
+
if (value._bsontype) {
|
|
151
|
+
return getDefaultValueForDescriptor(descriptor);
|
|
152
|
+
}
|
|
153
|
+
const result = {};
|
|
154
|
+
for (const [innerKey, innerValue] of Object.entries(value)) {
|
|
155
|
+
result[innerKey] = sanitizeExampleValue(innerValue, { type: detectBlueprintType(innerValue) });
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getDefaultValueForDescriptor(descriptor, key = '') {
|
|
164
|
+
const baseValue = (() => {
|
|
165
|
+
switch (descriptor.type) {
|
|
166
|
+
case 'number':
|
|
167
|
+
return 0;
|
|
168
|
+
case 'boolean':
|
|
169
|
+
return false;
|
|
170
|
+
case 'datetime':
|
|
171
|
+
case 'date':
|
|
172
|
+
case 'time':
|
|
173
|
+
return new Date().toISOString();
|
|
174
|
+
case 'object':
|
|
175
|
+
return {};
|
|
176
|
+
case 'file':
|
|
177
|
+
return 'https://example.com/file';
|
|
178
|
+
default:
|
|
179
|
+
return `Sample ${formatTitle(key || 'value')}`;
|
|
180
|
+
}
|
|
181
|
+
})();
|
|
182
|
+
|
|
183
|
+
if (descriptor.multi) {
|
|
184
|
+
return [baseValue];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return baseValue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function stringifyObjectLiteral(value, level = 0) {
|
|
191
|
+
const indent = ' '.repeat(level + 2);
|
|
192
|
+
const baseIndent = ' '.repeat(level);
|
|
193
|
+
|
|
194
|
+
if (Array.isArray(value)) {
|
|
195
|
+
if (value.length === 0) {
|
|
196
|
+
return '[]';
|
|
197
|
+
}
|
|
198
|
+
const items = value
|
|
199
|
+
.map((item) => `${indent}${stringifyObjectLiteral(item, level + 2)}`)
|
|
200
|
+
.join(',\n');
|
|
201
|
+
return `[\n${items}\n${baseIndent}]`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (value && typeof value === 'object') {
|
|
205
|
+
const entries = Object.entries(value);
|
|
206
|
+
if (entries.length === 0) {
|
|
207
|
+
return '{}';
|
|
208
|
+
}
|
|
209
|
+
const lines = entries.map(
|
|
210
|
+
([key, val]) => `${indent}${key}: ${stringifyObjectLiteral(val, level + 2)},`
|
|
211
|
+
);
|
|
212
|
+
return `\n${indent ? baseIndent : ''}{\n${lines.join('\n')}\n${baseIndent}}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (typeof value === 'string') {
|
|
216
|
+
return JSON.stringify(value);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return String(value);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function toCamelCase(value) {
|
|
223
|
+
return (value || '')
|
|
224
|
+
.replace(/[-_\s]+(.)?/g, (_, chr) => (chr ? chr.toUpperCase() : ''))
|
|
225
|
+
.replace(/^(.)/, (match) => match.toLowerCase());
|
|
226
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export function formatTitle(value) {
|
|
2
|
+
return (value || '')
|
|
3
|
+
.replace(/[_-]+/g, ' ')
|
|
4
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
5
|
+
.replace(/\s+/g, ' ')
|
|
6
|
+
.trim()
|
|
7
|
+
.replace(/\b\w/g, (match) => match.toUpperCase());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function toIdentifier(collectionName) {
|
|
11
|
+
const sanitized = (collectionName || '')
|
|
12
|
+
.trim()
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
15
|
+
.replace(/^_+|_+$/g, '');
|
|
16
|
+
|
|
17
|
+
return sanitized || `collection_${Date.now()}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ensureSingular(value = '') {
|
|
21
|
+
const normalized = value.trim();
|
|
22
|
+
const lower = normalized.toLowerCase();
|
|
23
|
+
|
|
24
|
+
if (lower.endsWith('ies')) {
|
|
25
|
+
return normalized.slice(0, -3) + normalized.slice(-3).replace(/ies$/i, 'y');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (/(sses|xes|zes|ches|shes)$/i.test(lower)) {
|
|
29
|
+
return normalized.slice(0, -2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (lower.endsWith('s') && !lower.endsWith('ss')) {
|
|
33
|
+
return normalized.slice(0, -1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return normalized;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function detectBlueprintType(value) {
|
|
40
|
+
if (value === null || value === undefined) {
|
|
41
|
+
return 'string';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (value instanceof Date) {
|
|
45
|
+
return 'datetime';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof value === 'number') {
|
|
49
|
+
return 'number';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof value === 'boolean') {
|
|
53
|
+
return 'boolean';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof value === 'object') {
|
|
57
|
+
if (value?._bsontype === 'ObjectId') {
|
|
58
|
+
return 'string';
|
|
59
|
+
}
|
|
60
|
+
return 'object';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return 'string';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function mapBlueprintTypeToJsonSchema(type) {
|
|
67
|
+
switch (type) {
|
|
68
|
+
case 'number':
|
|
69
|
+
return 'number';
|
|
70
|
+
case 'boolean':
|
|
71
|
+
return 'boolean';
|
|
72
|
+
case 'object':
|
|
73
|
+
return 'object';
|
|
74
|
+
default:
|
|
75
|
+
return 'string';
|
|
76
|
+
}
|
|
77
|
+
}
|
package/services/components.mjs
CHANGED
|
@@ -41,16 +41,18 @@ export async function pushComponents(sdk, path, options = {}) {
|
|
|
41
41
|
const componentName = file.replace('.vue', '');
|
|
42
42
|
const info = componentsJson[componentName] || {};
|
|
43
43
|
const content = fs.readFileSync(join(path, file), 'utf-8');
|
|
44
|
+
const targetIdentifier = info.identifier || componentName;
|
|
45
|
+
const targetDescription = info.description || 'Component description';
|
|
44
46
|
|
|
45
47
|
logger.step(`Pushing component: ${componentName}`);
|
|
46
48
|
|
|
47
49
|
const existingComponent = existingComponents.find(
|
|
48
|
-
component => component.identifier === componentName
|
|
50
|
+
component => component.identifier === targetIdentifier || component.componentName === componentName
|
|
49
51
|
);
|
|
50
52
|
|
|
51
53
|
if (existingComponent) {
|
|
52
54
|
await sdk.components.update(existingComponent._id, {
|
|
53
|
-
identifier:
|
|
55
|
+
identifier: targetIdentifier,
|
|
54
56
|
componentName: componentName,
|
|
55
57
|
content,
|
|
56
58
|
description: info.description || existingComponent.description || 'Component description'
|
|
@@ -58,10 +60,10 @@ export async function pushComponents(sdk, path, options = {}) {
|
|
|
58
60
|
logger.success(`Updated: ${componentName}`);
|
|
59
61
|
} else {
|
|
60
62
|
await sdk.components.create({
|
|
61
|
-
identifier:
|
|
63
|
+
identifier: targetIdentifier,
|
|
62
64
|
componentName: componentName,
|
|
63
65
|
content,
|
|
64
|
-
description:
|
|
66
|
+
description: targetDescription
|
|
65
67
|
});
|
|
66
68
|
logger.success(`Created: ${componentName}`);
|
|
67
69
|
}
|