@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.
@@ -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
- const SUPPORTED_PROTOCOL = /^mongodb:\/\//i;
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
- ensureDirectory(targetDir);
19
-
20
- const client = new MongoClient(connectionUri, {
21
- serverSelectionTimeoutMS: 10_000,
22
- });
17
+ const shouldGenerateGuides = guides !== false;
23
18
 
24
19
  try {
25
- await client.connect();
26
- const dbName = getDatabaseName(connectionUri);
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
- `${blueprint.identifier}.blueprint.json`
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
- return undefined;
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.15",
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.0",
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
+ }
@@ -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: info.identifier || existingComponent.identifier || componentName,
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: info.identifier || componentName,
63
+ identifier: targetIdentifier,
62
64
  componentName: componentName,
63
65
  content,
64
- description: info.description || 'Component description'
66
+ description: targetDescription
65
67
  });
66
68
  logger.success(`Created: ${componentName}`);
67
69
  }