@qelos/plugins-cli 0.0.14 → 0.0.16
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/cli.mjs +2 -0
- package/commands/blueprints.mjs +28 -0
- package/controllers/blueprints.mjs +28 -0
- package/controllers/create.mjs +1 -1
- package/controllers/generate.mjs +0 -2
- package/controllers/push.mjs +21 -11
- package/package.json +2 -1
- package/services/blocks.mjs +9 -3
- package/services/blueprint-generator.mjs +247 -0
- package/services/blueprint-sdk-guides.mjs +226 -0
- package/services/blueprint-shared.mjs +77 -0
- package/services/blueprints.mjs +9 -3
- package/services/components.mjs +10 -4
- package/services/configurations.mjs +9 -3
- package/services/plugins.mjs +9 -3
package/cli.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import createCommand from './commands/create.mjs';
|
|
|
10
10
|
import pushCommand from './commands/push.mjs';
|
|
11
11
|
import pullCommand from './commands/pull.mjs';
|
|
12
12
|
import generateCommand from './commands/generate.mjs';
|
|
13
|
+
import blueprintsCommand from './commands/blueprints.mjs';
|
|
13
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
|
|
15
16
|
const program = yargs(hideBin(process.argv));
|
|
@@ -26,5 +27,6 @@ createCommand(program)
|
|
|
26
27
|
pushCommand(program)
|
|
27
28
|
pullCommand(program)
|
|
28
29
|
generateCommand(program)
|
|
30
|
+
blueprintsCommand(program)
|
|
29
31
|
|
|
30
32
|
program.help().argv;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import blueprintsController from "../controllers/blueprints.mjs";
|
|
2
|
+
|
|
3
|
+
export default function blueprintsCommand(program) {
|
|
4
|
+
program
|
|
5
|
+
.command('blueprints generate [path]', 'generate new blueprints from actual database tables/collections',
|
|
6
|
+
(yargs) => {
|
|
7
|
+
return yargs
|
|
8
|
+
.positional('path', {
|
|
9
|
+
describe: 'Path to store the pulled resources.',
|
|
10
|
+
type: 'string',
|
|
11
|
+
default: './blueprints',
|
|
12
|
+
required: false
|
|
13
|
+
})
|
|
14
|
+
.option('uri', {
|
|
15
|
+
describe: 'The URI of the database. Defaults to mongodb://localhost:27017/db',
|
|
16
|
+
type: 'string',
|
|
17
|
+
required: false,
|
|
18
|
+
default: 'mongodb://localhost:27017/db'
|
|
19
|
+
})
|
|
20
|
+
.option('guides', {
|
|
21
|
+
describe: 'Generate SDK guides for each blueprint',
|
|
22
|
+
type: 'boolean',
|
|
23
|
+
required: false,
|
|
24
|
+
default: true
|
|
25
|
+
})
|
|
26
|
+
},
|
|
27
|
+
blueprintsController)
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { logger } from "../services/logger.mjs";
|
|
3
|
+
import {
|
|
4
|
+
SUPPORTED_PROTOCOL,
|
|
5
|
+
generateBlueprintsFromMongo,
|
|
6
|
+
} from "../services/blueprint-generator.mjs";
|
|
7
|
+
|
|
8
|
+
export default async function blueprintsController({ uri, path: targetPath, guides = true }) {
|
|
9
|
+
const connectionUri = uri || "mongodb://localhost:27017/db";
|
|
10
|
+
|
|
11
|
+
if (!SUPPORTED_PROTOCOL.test(connectionUri)) {
|
|
12
|
+
logger.error("Only mongodb:// URIs are supported at the moment.");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const targetDir = path.join(process.cwd(), targetPath);
|
|
17
|
+
const shouldGenerateGuides = guides !== false;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
await generateBlueprintsFromMongo({
|
|
21
|
+
uri: connectionUri,
|
|
22
|
+
targetDir,
|
|
23
|
+
createGuides: shouldGenerateGuides,
|
|
24
|
+
});
|
|
25
|
+
} catch {
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
package/controllers/create.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import follow from "follow-redirects";
|
|
|
2
2
|
import cliSelect from "cli-select";
|
|
3
3
|
import { blue } from "../utils/colors.mjs";
|
|
4
4
|
import DecompressZip from "decompress-zip";
|
|
5
|
-
import { join } from "path";
|
|
5
|
+
import { join } from "node:path";
|
|
6
6
|
import { rimraf } from "rimraf";
|
|
7
7
|
import ProgressBar from "../utils/progress-bar.mjs";
|
|
8
8
|
import * as readline from "node:readline";
|
package/controllers/generate.mjs
CHANGED
package/controllers/push.mjs
CHANGED
|
@@ -17,10 +17,16 @@ export default async function pushController({ type, path: sourcePath }) {
|
|
|
17
17
|
process.exit(1);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const stat = fs.statSync(sourcePath);
|
|
21
|
+
let basePath = sourcePath;
|
|
22
|
+
let targetFile = null;
|
|
23
|
+
|
|
24
|
+
if (stat.isFile()) {
|
|
25
|
+
basePath = path.dirname(sourcePath);
|
|
26
|
+
targetFile = path.basename(sourcePath);
|
|
27
|
+
logger.info(`Detected file path. Only pushing ${targetFile}`);
|
|
28
|
+
} else if (!stat.isDirectory()) {
|
|
29
|
+
logger.error(`Path must be a file or directory: ${sourcePath}`);
|
|
24
30
|
process.exit(1);
|
|
25
31
|
}
|
|
26
32
|
|
|
@@ -28,6 +34,10 @@ export default async function pushController({ type, path: sourcePath }) {
|
|
|
28
34
|
|
|
29
35
|
// Handle "all" or "*" type
|
|
30
36
|
if (type === 'all' || type === '*') {
|
|
37
|
+
if (targetFile) {
|
|
38
|
+
logger.error('Cannot push "all" using a single file. Please provide a directory path.');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
31
41
|
logger.section(`Pushing all resources from ${sourcePath}`);
|
|
32
42
|
|
|
33
43
|
const types = [
|
|
@@ -39,7 +49,7 @@ export default async function pushController({ type, path: sourcePath }) {
|
|
|
39
49
|
];
|
|
40
50
|
|
|
41
51
|
for (const { name, fn } of types) {
|
|
42
|
-
const typePath = path.join(
|
|
52
|
+
const typePath = path.join(basePath, name);
|
|
43
53
|
|
|
44
54
|
// Skip if directory doesn't exist
|
|
45
55
|
if (!fs.existsSync(typePath)) {
|
|
@@ -60,18 +70,18 @@ export default async function pushController({ type, path: sourcePath }) {
|
|
|
60
70
|
return;
|
|
61
71
|
}
|
|
62
72
|
|
|
63
|
-
logger.section(`Pushing ${type} from ${
|
|
73
|
+
logger.section(`Pushing ${type} from ${targetFile ? `${basePath} (${targetFile})` : basePath}`);
|
|
64
74
|
|
|
65
75
|
if (type === 'components') {
|
|
66
|
-
await pushComponents(sdk,
|
|
76
|
+
await pushComponents(sdk, basePath, { targetFile });
|
|
67
77
|
} else if (type === 'blueprints') {
|
|
68
|
-
await pushBlueprints(sdk,
|
|
78
|
+
await pushBlueprints(sdk, basePath, { targetFile });
|
|
69
79
|
} else if (type === 'plugins') {
|
|
70
|
-
await pushPlugins(sdk,
|
|
80
|
+
await pushPlugins(sdk, basePath, { targetFile });
|
|
71
81
|
} else if (type === 'blocks') {
|
|
72
|
-
await pushBlocks(sdk,
|
|
82
|
+
await pushBlocks(sdk, basePath, { targetFile });
|
|
73
83
|
} else if (type === 'config' || type === 'configs' || type === 'configuration') {
|
|
74
|
-
await pushConfigurations(sdk,
|
|
84
|
+
await pushConfigurations(sdk, basePath, { targetFile });
|
|
75
85
|
} else {
|
|
76
86
|
logger.error(`Unknown type: ${type}`);
|
|
77
87
|
logger.info('Supported types: components, blueprints, plugins, blocks, config, configs, configuration, all');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qelos/plugins-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"description": "CLI to manage QELOS plugins",
|
|
5
5
|
"main": "cli.mjs",
|
|
6
6
|
"bin": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"decompress-zip": "^0.3.3",
|
|
21
21
|
"follow-redirects": "^1.15.11",
|
|
22
22
|
"jiti": "^2.6.1",
|
|
23
|
+
"mongodb": "^7.0.0",
|
|
23
24
|
"rimraf": "^6.0.1",
|
|
24
25
|
"yargs": "^18.0.0",
|
|
25
26
|
"zx": "^8.8.5"
|
package/services/blocks.mjs
CHANGED
|
@@ -17,12 +17,18 @@ function toKebabCase(str) {
|
|
|
17
17
|
* @param {Object} sdk - Initialized SDK instance
|
|
18
18
|
* @param {string} path - Path to blocks directory
|
|
19
19
|
*/
|
|
20
|
-
export async function pushBlocks(sdk, path) {
|
|
21
|
-
const
|
|
20
|
+
export async function pushBlocks(sdk, path, options = {}) {
|
|
21
|
+
const { targetFile } = options;
|
|
22
|
+
const directoryFiles = fs.readdirSync(path);
|
|
23
|
+
const files = targetFile ? [targetFile] : directoryFiles;
|
|
22
24
|
const blockFiles = files.filter(f => f.endsWith('.html'));
|
|
23
25
|
|
|
24
26
|
if (blockFiles.length === 0) {
|
|
25
|
-
|
|
27
|
+
if (targetFile) {
|
|
28
|
+
logger.warning(`File ${targetFile} is not an .html block. Skipping.`);
|
|
29
|
+
} else {
|
|
30
|
+
logger.warning(`No .html files found in ${path}`);
|
|
31
|
+
}
|
|
26
32
|
return;
|
|
27
33
|
}
|
|
28
34
|
|
|
@@ -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/blueprints.mjs
CHANGED
|
@@ -7,12 +7,18 @@ import { logger } from './logger.mjs';
|
|
|
7
7
|
* @param {Object} sdk - Initialized SDK instance
|
|
8
8
|
* @param {string} path - Path to blueprints directory
|
|
9
9
|
*/
|
|
10
|
-
export async function pushBlueprints(sdk, path) {
|
|
11
|
-
const
|
|
10
|
+
export async function pushBlueprints(sdk, path, options = {}) {
|
|
11
|
+
const { targetFile } = options;
|
|
12
|
+
const directoryFiles = fs.readdirSync(path);
|
|
13
|
+
const files = targetFile ? [targetFile] : directoryFiles;
|
|
12
14
|
const blueprintFiles = files.filter(f => f.endsWith('.blueprint.json'));
|
|
13
15
|
|
|
14
16
|
if (blueprintFiles.length === 0) {
|
|
15
|
-
|
|
17
|
+
if (targetFile) {
|
|
18
|
+
logger.warning(`File ${targetFile} is not a .blueprint.json file. Skipping.`);
|
|
19
|
+
} else {
|
|
20
|
+
logger.warning(`No blueprint files (*.blueprint.json) found in ${path}`);
|
|
21
|
+
}
|
|
16
22
|
return;
|
|
17
23
|
}
|
|
18
24
|
|
package/services/components.mjs
CHANGED
|
@@ -7,12 +7,18 @@ import { logger } from './logger.mjs';
|
|
|
7
7
|
* @param {Object} sdk - Initialized SDK instance
|
|
8
8
|
* @param {string} path - Path to components directory
|
|
9
9
|
*/
|
|
10
|
-
export async function pushComponents(sdk, path) {
|
|
11
|
-
const
|
|
10
|
+
export async function pushComponents(sdk, path, options = {}) {
|
|
11
|
+
const { targetFile } = options;
|
|
12
|
+
const directoryFiles = fs.readdirSync(path);
|
|
13
|
+
const files = targetFile ? [targetFile] : directoryFiles;
|
|
12
14
|
const vueFiles = files.filter(f => f.endsWith('.vue'));
|
|
13
15
|
|
|
14
16
|
if (vueFiles.length === 0) {
|
|
15
|
-
|
|
17
|
+
if (targetFile) {
|
|
18
|
+
logger.warning(`File ${targetFile} is not a .vue component. Skipping.`);
|
|
19
|
+
} else {
|
|
20
|
+
logger.warning(`No .vue files found in ${path}`);
|
|
21
|
+
}
|
|
16
22
|
return;
|
|
17
23
|
}
|
|
18
24
|
|
|
@@ -30,7 +36,7 @@ export async function pushComponents(sdk, path) {
|
|
|
30
36
|
|
|
31
37
|
const existingComponents = await sdk.components.getList();
|
|
32
38
|
|
|
33
|
-
await Promise.all(
|
|
39
|
+
await Promise.all(vueFiles.map(async (file) => {
|
|
34
40
|
if (file.endsWith('.vue')) {
|
|
35
41
|
const componentName = file.replace('.vue', '');
|
|
36
42
|
const info = componentsJson[componentName] || {};
|
|
@@ -8,12 +8,18 @@ import { appUrl } from './sdk.mjs';
|
|
|
8
8
|
* @param {Object} sdk - Initialized SDK instance
|
|
9
9
|
* @param {string} path - Path to configurations directory
|
|
10
10
|
*/
|
|
11
|
-
export async function pushConfigurations(sdk, path) {
|
|
12
|
-
const
|
|
11
|
+
export async function pushConfigurations(sdk, path, options = {}) {
|
|
12
|
+
const { targetFile } = options;
|
|
13
|
+
const directoryFiles = fs.readdirSync(path);
|
|
14
|
+
const files = targetFile ? [targetFile] : directoryFiles;
|
|
13
15
|
const configFiles = files.filter(f => f.endsWith('.config.json'));
|
|
14
16
|
|
|
15
17
|
if (configFiles.length === 0) {
|
|
16
|
-
|
|
18
|
+
if (targetFile) {
|
|
19
|
+
logger.warning(`File ${targetFile} is not a .config.json file. Skipping.`);
|
|
20
|
+
} else {
|
|
21
|
+
logger.warning(`No configuration files (*.config.json) found in ${path}`);
|
|
22
|
+
}
|
|
17
23
|
return;
|
|
18
24
|
}
|
|
19
25
|
|
package/services/plugins.mjs
CHANGED
|
@@ -9,12 +9,18 @@ import { extractMicroFrontendStructures, resolveMicroFrontendStructures } from '
|
|
|
9
9
|
* @param {Object} sdk - Initialized SDK instance
|
|
10
10
|
* @param {string} path - Path to plugins directory
|
|
11
11
|
*/
|
|
12
|
-
export async function pushPlugins(sdk, path) {
|
|
13
|
-
const
|
|
12
|
+
export async function pushPlugins(sdk, path, options = {}) {
|
|
13
|
+
const { targetFile } = options;
|
|
14
|
+
const directoryFiles = fs.readdirSync(path);
|
|
15
|
+
const files = targetFile ? [targetFile] : directoryFiles;
|
|
14
16
|
const pluginFiles = files.filter(f => f.endsWith('.plugin.json'));
|
|
15
17
|
|
|
16
18
|
if (pluginFiles.length === 0) {
|
|
17
|
-
|
|
19
|
+
if (targetFile) {
|
|
20
|
+
logger.warning(`File ${targetFile} is not a .plugin.json file. Skipping.`);
|
|
21
|
+
} else {
|
|
22
|
+
logger.warning(`No plugin files (*.plugin.json) found in ${path}`);
|
|
23
|
+
}
|
|
18
24
|
return;
|
|
19
25
|
}
|
|
20
26
|
|