@open-mercato/cli 0.4.2-canary-c02407ff85
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/bin/mercato +21 -0
- package/build.mjs +78 -0
- package/dist/bin.js +51 -0
- package/dist/bin.js.map +7 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +7 -0
- package/dist/lib/db/commands.js +350 -0
- package/dist/lib/db/commands.js.map +7 -0
- package/dist/lib/db/index.js +7 -0
- package/dist/lib/db/index.js.map +7 -0
- package/dist/lib/generators/entity-ids.js +257 -0
- package/dist/lib/generators/entity-ids.js.map +7 -0
- package/dist/lib/generators/index.js +12 -0
- package/dist/lib/generators/index.js.map +7 -0
- package/dist/lib/generators/module-di.js +73 -0
- package/dist/lib/generators/module-di.js.map +7 -0
- package/dist/lib/generators/module-entities.js +104 -0
- package/dist/lib/generators/module-entities.js.map +7 -0
- package/dist/lib/generators/module-registry.js +1081 -0
- package/dist/lib/generators/module-registry.js.map +7 -0
- package/dist/lib/resolver.js +205 -0
- package/dist/lib/resolver.js.map +7 -0
- package/dist/lib/utils.js +161 -0
- package/dist/lib/utils.js.map +7 -0
- package/dist/mercato.js +1045 -0
- package/dist/mercato.js.map +7 -0
- package/dist/registry.js +7 -0
- package/dist/registry.js.map +7 -0
- package/jest.config.cjs +19 -0
- package/package.json +71 -0
- package/src/__tests__/mercato.test.ts +90 -0
- package/src/bin.ts +74 -0
- package/src/index.ts +2 -0
- package/src/lib/__tests__/resolver.test.ts +101 -0
- package/src/lib/__tests__/utils.test.ts +270 -0
- package/src/lib/db/__tests__/commands.test.ts +131 -0
- package/src/lib/db/commands.ts +431 -0
- package/src/lib/db/index.ts +1 -0
- package/src/lib/generators/__tests__/generators.test.ts +197 -0
- package/src/lib/generators/entity-ids.ts +336 -0
- package/src/lib/generators/index.ts +4 -0
- package/src/lib/generators/module-di.ts +89 -0
- package/src/lib/generators/module-entities.ts +124 -0
- package/src/lib/generators/module-registry.ts +1222 -0
- package/src/lib/resolver.ts +308 -0
- package/src/lib/utils.ts +200 -0
- package/src/mercato.ts +1106 -0
- package/src/registry.ts +2 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +12 -0
- package/watch.mjs +6 -0
package/dist/mercato.js
ADDED
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
2
|
+
import { runWorker } from "@open-mercato/queue/worker";
|
|
3
|
+
import { getCliModules, hasCliModules, registerCliModules } from "./registry.js";
|
|
4
|
+
import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
let envLoaded = false;
|
|
8
|
+
async function ensureEnvLoaded() {
|
|
9
|
+
if (envLoaded) return;
|
|
10
|
+
envLoaded = true;
|
|
11
|
+
try {
|
|
12
|
+
const { createResolver } = await import("./lib/resolver.js");
|
|
13
|
+
const resolver = createResolver();
|
|
14
|
+
const appDir = resolver.getAppDir();
|
|
15
|
+
const envPath = path.join(appDir, ".env");
|
|
16
|
+
if (fs.existsSync(envPath)) {
|
|
17
|
+
const dotenv = await import("dotenv");
|
|
18
|
+
dotenv.config({ path: envPath });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
await import("dotenv/config");
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function runModuleCommand(allModules, moduleName, commandName, args = []) {
|
|
29
|
+
const mod = allModules.find((m) => m.id === moduleName);
|
|
30
|
+
if (!mod) {
|
|
31
|
+
throw new Error(`Module not found: "${moduleName}"`);
|
|
32
|
+
}
|
|
33
|
+
if (!mod.cli || mod.cli.length === 0) {
|
|
34
|
+
throw new Error(`Module "${moduleName}" has no CLI commands`);
|
|
35
|
+
}
|
|
36
|
+
const cmd = mod.cli.find((c) => c.command === commandName);
|
|
37
|
+
if (!cmd) {
|
|
38
|
+
throw new Error(`Command "${commandName}" not found in module "${moduleName}"`);
|
|
39
|
+
}
|
|
40
|
+
await cmd.run(args);
|
|
41
|
+
}
|
|
42
|
+
async function buildAllModules() {
|
|
43
|
+
const modules = getCliModules();
|
|
44
|
+
let appCli = [];
|
|
45
|
+
try {
|
|
46
|
+
const dynImport = Function("return import")();
|
|
47
|
+
const app = await dynImport.then((f) => f("@/cli")).catch(() => null);
|
|
48
|
+
if (app && Array.isArray(app?.default)) appCli = app.default;
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
const all = modules.slice();
|
|
52
|
+
if (appCli.length) all.push({ id: "app", cli: appCli });
|
|
53
|
+
return all;
|
|
54
|
+
}
|
|
55
|
+
async function run(argv = process.argv) {
|
|
56
|
+
await ensureEnvLoaded();
|
|
57
|
+
const [, , ...parts] = argv;
|
|
58
|
+
const [first, second, ...remaining] = parts;
|
|
59
|
+
if (first === "init") {
|
|
60
|
+
const { execSync } = await import("child_process");
|
|
61
|
+
console.log("\u{1F680} Initializing Open Mercato app...\n");
|
|
62
|
+
try {
|
|
63
|
+
const initArgs = parts.slice(1).filter(Boolean);
|
|
64
|
+
const reinstall = initArgs.includes("--reinstall") || initArgs.includes("-r");
|
|
65
|
+
const skipExamples = initArgs.includes("--no-examples") || initArgs.includes("--no-exampls");
|
|
66
|
+
const stressTestEnabled = initArgs.includes("--stresstest") || initArgs.includes("--stress-test");
|
|
67
|
+
const stressTestLite = initArgs.includes("--lite") || initArgs.includes("--stress-lite") || initArgs.some((arg) => arg.startsWith("--payload=lite") || arg.startsWith("--mode=lite"));
|
|
68
|
+
let stressTestCount = 6e3;
|
|
69
|
+
for (let i = 0; i < initArgs.length; i += 1) {
|
|
70
|
+
const arg = initArgs[i];
|
|
71
|
+
const countPrefixes = ["--count=", "--stress-count=", "--stresstest-count="];
|
|
72
|
+
const matchedPrefix = countPrefixes.find((prefix) => arg.startsWith(prefix));
|
|
73
|
+
if (matchedPrefix) {
|
|
74
|
+
const value = arg.slice(matchedPrefix.length);
|
|
75
|
+
const parsed = Number.parseInt(value, 10);
|
|
76
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
77
|
+
stressTestCount = parsed;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (arg === "--count" || arg === "--stress-count" || arg === "--stresstest-count" || arg === "-n") {
|
|
82
|
+
const next = initArgs[i + 1];
|
|
83
|
+
if (next && !next.startsWith("-")) {
|
|
84
|
+
const parsed = Number.parseInt(next, 10);
|
|
85
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
86
|
+
stressTestCount = parsed;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (arg.startsWith("-n=")) {
|
|
92
|
+
const value = arg.slice(3);
|
|
93
|
+
const parsed = Number.parseInt(value, 10);
|
|
94
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
95
|
+
stressTestCount = parsed;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
console.log(`\u{1F504} Reinstall mode: ${reinstall ? "enabled" : "disabled"}`);
|
|
101
|
+
console.log(`\u{1F3A8} Example content: ${skipExamples ? "skipped (--no-examples)" : "enabled"}`);
|
|
102
|
+
console.log(
|
|
103
|
+
`\u{1F3CB}\uFE0F Stress test dataset: ${stressTestEnabled ? `enabled (target ${stressTestCount} contacts${stressTestLite ? ", lite payload" : ""})` : "disabled"}`
|
|
104
|
+
);
|
|
105
|
+
if (reinstall) {
|
|
106
|
+
await ensureEnvLoaded();
|
|
107
|
+
console.log("\u267B\uFE0F Reinstall mode enabled: dropping all database tables...");
|
|
108
|
+
const { Client: Client2 } = await import("pg");
|
|
109
|
+
const dbUrl2 = process.env.DATABASE_URL;
|
|
110
|
+
if (!dbUrl2) {
|
|
111
|
+
console.error("DATABASE_URL is not set. Aborting reinstall.");
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
const client = new Client2({ connectionString: dbUrl2 });
|
|
115
|
+
try {
|
|
116
|
+
await client.connect();
|
|
117
|
+
const res = await client.query(`SELECT tablename FROM pg_tables WHERE schemaname = 'public'`);
|
|
118
|
+
const dropTargets = new Set((res.rows || []).map((r) => String(r.tablename)));
|
|
119
|
+
for (const forced of ["vector_search", "vector_search_migrations"]) {
|
|
120
|
+
const exists = await client.query(
|
|
121
|
+
`SELECT to_regclass($1) AS regclass`,
|
|
122
|
+
[`public.${forced}`]
|
|
123
|
+
);
|
|
124
|
+
const regclass = exists.rows?.[0]?.regclass ?? null;
|
|
125
|
+
if (regclass) {
|
|
126
|
+
dropTargets.add(forced);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (dropTargets.size === 0) {
|
|
130
|
+
console.log(" No tables found in public schema.");
|
|
131
|
+
} else {
|
|
132
|
+
let dropped = 0;
|
|
133
|
+
await client.query("BEGIN");
|
|
134
|
+
try {
|
|
135
|
+
for (const t of dropTargets) {
|
|
136
|
+
await client.query(`DROP TABLE IF EXISTS "${t}" CASCADE`);
|
|
137
|
+
dropped += 1;
|
|
138
|
+
}
|
|
139
|
+
await client.query("COMMIT");
|
|
140
|
+
console.log(` Dropped ${dropped} tables.`);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
await client.query("ROLLBACK");
|
|
143
|
+
throw e;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} finally {
|
|
147
|
+
try {
|
|
148
|
+
await client.end();
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const Redis = (await import("ioredis")).default;
|
|
154
|
+
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
|
155
|
+
const redis = new Redis(redisUrl);
|
|
156
|
+
await redis.flushall();
|
|
157
|
+
await redis.quit();
|
|
158
|
+
console.log(" Redis flushed.");
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
console.log("\u2705 Database cleared. Proceeding with fresh initialization...\n");
|
|
162
|
+
}
|
|
163
|
+
console.log("\u{1F527} Preparing modules (registry, entities, DI)...");
|
|
164
|
+
const { createResolver } = await import("./lib/resolver.js");
|
|
165
|
+
const { generateEntityIds, generateModuleRegistry, generateModuleRegistryCli, generateModuleEntities, generateModuleDi } = await import("./lib/generators/index.js");
|
|
166
|
+
const resolver = createResolver();
|
|
167
|
+
await generateEntityIds({ resolver, quiet: true });
|
|
168
|
+
await generateModuleRegistry({ resolver, quiet: true });
|
|
169
|
+
await generateModuleRegistryCli({ resolver, quiet: true });
|
|
170
|
+
await generateModuleEntities({ resolver, quiet: true });
|
|
171
|
+
await generateModuleDi({ resolver, quiet: true });
|
|
172
|
+
console.log("\u2705 Modules prepared\n");
|
|
173
|
+
console.log("\u{1F4CA} Applying database migrations...");
|
|
174
|
+
const { dbMigrate } = await import("./lib/db/index.js");
|
|
175
|
+
await dbMigrate(resolver);
|
|
176
|
+
console.log("\u2705 Migrations applied\n");
|
|
177
|
+
console.log("\u{1F517} Bootstrapping application...");
|
|
178
|
+
const { bootstrapFromAppRoot } = await import("@open-mercato/shared/lib/bootstrap/dynamicLoader");
|
|
179
|
+
const bootstrapData = await bootstrapFromAppRoot(resolver.getAppDir());
|
|
180
|
+
registerCliModules(bootstrapData.modules);
|
|
181
|
+
console.log("\u2705 Bootstrap complete\n");
|
|
182
|
+
const allModules = await buildAllModules();
|
|
183
|
+
console.log("\u2699\uFE0F Restoring module defaults...");
|
|
184
|
+
await runModuleCommand(allModules, "configs", "restore-defaults", []);
|
|
185
|
+
console.log("\u2705 Module defaults restored\n");
|
|
186
|
+
const findArgValue = (names, fallback) => {
|
|
187
|
+
for (const name of names) {
|
|
188
|
+
const match = initArgs.find((arg) => arg.startsWith(name));
|
|
189
|
+
if (match) {
|
|
190
|
+
const value = match.slice(name.length);
|
|
191
|
+
if (value) return value;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return fallback;
|
|
195
|
+
};
|
|
196
|
+
const orgName = findArgValue(["--org=", "--orgName="], "Acme Corp");
|
|
197
|
+
const email = findArgValue(["--email="], "superadmin@acme.com");
|
|
198
|
+
const password = findArgValue(["--password="], "secret");
|
|
199
|
+
const roles = findArgValue(["--roles="], "superadmin,admin,employee");
|
|
200
|
+
console.log("\u{1F510} Setting up RBAC and users...");
|
|
201
|
+
await runModuleCommand(allModules, "auth", "setup", [
|
|
202
|
+
"--orgName",
|
|
203
|
+
orgName,
|
|
204
|
+
"--email",
|
|
205
|
+
email,
|
|
206
|
+
"--password",
|
|
207
|
+
password,
|
|
208
|
+
"--roles",
|
|
209
|
+
roles
|
|
210
|
+
]);
|
|
211
|
+
const { Client } = await import("pg");
|
|
212
|
+
const dbUrl = process.env.DATABASE_URL;
|
|
213
|
+
const pgClient = new Client({ connectionString: dbUrl });
|
|
214
|
+
await pgClient.connect();
|
|
215
|
+
const orgResult = await pgClient.query(
|
|
216
|
+
`SELECT o.id as org_id, o.tenant_id FROM organizations o
|
|
217
|
+
JOIN users u ON u.organization_id = o.id
|
|
218
|
+
LIMIT 1`
|
|
219
|
+
);
|
|
220
|
+
await pgClient.end();
|
|
221
|
+
const tenantId = orgResult?.rows?.[0]?.tenant_id ?? null;
|
|
222
|
+
const orgId = orgResult?.rows?.[0]?.org_id ?? null;
|
|
223
|
+
console.log("\u2705 RBAC setup complete:", { tenantId, organizationId: orgId }, "\n");
|
|
224
|
+
console.log("\u{1F39B}\uFE0F Seeding feature toggle defaults...");
|
|
225
|
+
await runModuleCommand(allModules, "feature_toggles", "seed-defaults", []);
|
|
226
|
+
console.log("\u{1F39B}\uFE0F \u2705 Feature toggle defaults seeded\n");
|
|
227
|
+
if (tenantId) {
|
|
228
|
+
console.log("\u{1F465} Seeding tenant-scoped roles...");
|
|
229
|
+
await runModuleCommand(allModules, "auth", "seed-roles", ["--tenant", tenantId]);
|
|
230
|
+
console.log("\u{1F6E1}\uFE0F \u2705 Roles seeded\n");
|
|
231
|
+
} else {
|
|
232
|
+
console.log("\u26A0\uFE0F Skipping role seeding because tenant ID was not available.\n");
|
|
233
|
+
}
|
|
234
|
+
if (orgId && tenantId) {
|
|
235
|
+
if (reinstall) {
|
|
236
|
+
console.log("\u{1F9E9} Reinstalling custom field definitions...");
|
|
237
|
+
await runModuleCommand(allModules, "entities", "reinstall", ["--tenant", tenantId]);
|
|
238
|
+
console.log("\u{1F9E9} \u2705 Custom field definitions reinstalled\n");
|
|
239
|
+
}
|
|
240
|
+
console.log("\u{1F4DA} Seeding customer dictionaries...");
|
|
241
|
+
await runModuleCommand(allModules, "customers", "seed-dictionaries", ["--tenant", tenantId, "--org", orgId]);
|
|
242
|
+
console.log("\u{1F4DA} \u2705 Customer dictionaries seeded\n");
|
|
243
|
+
console.log("\u{1F3E0} Seeding staff address types...");
|
|
244
|
+
await runModuleCommand(allModules, "staff", "seed-address-types", ["--tenant", tenantId, "--org", orgId]);
|
|
245
|
+
console.log("\u{1F3E0} \u2705 Staff address types seeded\n");
|
|
246
|
+
console.log("\u{1F3E0} Seeding resources address types...");
|
|
247
|
+
await runModuleCommand(allModules, "resources", "seed-address-types", ["--tenant", tenantId, "--org", orgId]);
|
|
248
|
+
console.log("\u{1F3E0} \u2705 Resources address types seeded\n");
|
|
249
|
+
console.log("\u{1F4DA} Seeding currencies...");
|
|
250
|
+
await runModuleCommand(allModules, "currencies", "seed", ["--tenant", tenantId, "--org", orgId]);
|
|
251
|
+
console.log("\u{1F4DA} \u2705 Currencies seeded\n");
|
|
252
|
+
console.log("\u{1F4CF} Seeding catalog units...");
|
|
253
|
+
await runModuleCommand(allModules, "catalog", "seed-units", ["--tenant", tenantId, "--org", orgId]);
|
|
254
|
+
console.log("\u{1F4CF} \u2705 Catalog units seeded\n");
|
|
255
|
+
console.log("\u{1F5D3}\uFE0F Seeding unavailability reasons...");
|
|
256
|
+
await runModuleCommand(allModules, "planner", "seed-unavailability-reasons", ["--tenant", tenantId, "--org", orgId]);
|
|
257
|
+
console.log("\u{1F5D3}\uFE0F \u2705 Unavailability reasons seeded\n");
|
|
258
|
+
const parsedEncryption = parseBooleanToken(process.env.TENANT_DATA_ENCRYPTION ?? "yes");
|
|
259
|
+
const encryptionEnabled = parsedEncryption === null ? true : parsedEncryption;
|
|
260
|
+
if (encryptionEnabled) {
|
|
261
|
+
console.log("\u{1F512} Seeding encryption defaults...");
|
|
262
|
+
await runModuleCommand(allModules, "entities", "seed-encryption", ["--tenant", tenantId, "--org", orgId]);
|
|
263
|
+
console.log("\u{1F512} \u2705 Encryption defaults seeded\n");
|
|
264
|
+
} else {
|
|
265
|
+
console.log("\u26A0\uFE0F TENANT_DATA_ENCRYPTION disabled; skipping encryption defaults.\n");
|
|
266
|
+
}
|
|
267
|
+
console.log("\u{1F3F7}\uFE0F Seeding catalog price kinds...");
|
|
268
|
+
await runModuleCommand(allModules, "catalog", "seed-price-kinds", ["--tenant", tenantId, "--org", orgId]);
|
|
269
|
+
console.log("\u{1F3F7}\uFE0F \u2705 Catalog price kinds seeded\n");
|
|
270
|
+
console.log("\u{1F4B6} Seeding default tax rates...");
|
|
271
|
+
await runModuleCommand(allModules, "sales", "seed-tax-rates", ["--tenant", tenantId, "--org", orgId]);
|
|
272
|
+
console.log("\u{1F9FE} \u2705 Tax rates seeded\n");
|
|
273
|
+
console.log("\u{1F6A6} Seeding sales statuses...");
|
|
274
|
+
await runModuleCommand(allModules, "sales", "seed-statuses", ["--tenant", tenantId, "--org", orgId]);
|
|
275
|
+
console.log("\u{1F6A6} \u2705 Sales statuses seeded\n");
|
|
276
|
+
console.log("\u2699\uFE0F Seeding adjustment kinds...");
|
|
277
|
+
await runModuleCommand(allModules, "sales", "seed-adjustment-kinds", ["--tenant", tenantId, "--org", orgId]);
|
|
278
|
+
console.log("\u2699\uFE0F \u2705 Adjustment kinds seeded\n");
|
|
279
|
+
console.log("\u{1F69A} Seeding shipping methods...");
|
|
280
|
+
await runModuleCommand(allModules, "sales", "seed-shipping-methods", ["--tenant", tenantId, "--org", orgId]);
|
|
281
|
+
console.log("\u{1F69A} \u2705 Shipping methods seeded\n");
|
|
282
|
+
console.log("\u{1F4B3} Seeding payment methods...");
|
|
283
|
+
await runModuleCommand(allModules, "sales", "seed-payment-methods", ["--tenant", tenantId, "--org", orgId]);
|
|
284
|
+
console.log("\u{1F4B3} \u2705 Payment methods seeded\n");
|
|
285
|
+
console.log("\u{1F504} Seeding workflow definitions...");
|
|
286
|
+
try {
|
|
287
|
+
await runModuleCommand(allModules, "workflows", "seed-all", ["--tenant", tenantId, "--org", orgId]);
|
|
288
|
+
console.log("\u2705 Workflows and business rules seeded\n");
|
|
289
|
+
} catch (err) {
|
|
290
|
+
console.error("\u26A0\uFE0F Workflow seeding failed (non-fatal):", err);
|
|
291
|
+
}
|
|
292
|
+
if (skipExamples) {
|
|
293
|
+
console.log("\u{1F6AB} Example data seeding skipped (--no-examples)\n");
|
|
294
|
+
} else {
|
|
295
|
+
console.log("\u{1F6CD}\uFE0F Seeding catalog examples...");
|
|
296
|
+
await runModuleCommand(allModules, "catalog", "seed-examples", ["--tenant", tenantId, "--org", orgId]);
|
|
297
|
+
console.log("\u{1F6CD}\uFE0F \u2705 Catalog examples seeded\n");
|
|
298
|
+
console.log("\u{1F3E2} Seeding customer examples...");
|
|
299
|
+
await runModuleCommand(allModules, "customers", "seed-examples", ["--tenant", tenantId, "--org", orgId]);
|
|
300
|
+
console.log("\u{1F3E2} \u2705 Customer examples seeded\n");
|
|
301
|
+
console.log("\u{1F9FE} Seeding sales examples...");
|
|
302
|
+
await runModuleCommand(allModules, "sales", "seed-examples", ["--tenant", tenantId, "--org", orgId]);
|
|
303
|
+
console.log("\u{1F9FE} \u2705 Sales examples seeded\n");
|
|
304
|
+
console.log("\u{1F465} Seeding staff examples...");
|
|
305
|
+
await runModuleCommand(allModules, "staff", "seed-examples", ["--tenant", tenantId, "--org", orgId]);
|
|
306
|
+
console.log("\u{1F465} \u2705 Staff examples seeded\n");
|
|
307
|
+
console.log("\u{1F4E6} Seeding resource capacity units...");
|
|
308
|
+
await runModuleCommand(allModules, "resources", "seed-capacity-units", ["--tenant", tenantId, "--org", orgId]);
|
|
309
|
+
console.log("\u{1F4E6} \u2705 Resource capacity units seeded\n");
|
|
310
|
+
console.log("\u{1F9F0} Seeding resource examples...");
|
|
311
|
+
await runModuleCommand(allModules, "resources", "seed-examples", ["--tenant", tenantId, "--org", orgId]);
|
|
312
|
+
console.log("\u{1F9F0} \u2705 Resource examples seeded\n");
|
|
313
|
+
console.log("\u{1F5D3}\uFE0F Seeding planner availability rulesets...");
|
|
314
|
+
await runModuleCommand(allModules, "planner", "seed-availability-rulesets", ["--tenant", tenantId, "--org", orgId]);
|
|
315
|
+
console.log("\u{1F5D3}\uFE0F \u2705 Planner availability rulesets seeded\n");
|
|
316
|
+
const exampleModule = allModules.find((m) => m.id === "example");
|
|
317
|
+
if (exampleModule && exampleModule.cli) {
|
|
318
|
+
console.log("\u{1F4DD} Seeding example todos...");
|
|
319
|
+
await runModuleCommand(allModules, "example", "seed-todos", ["--org", orgId, "--tenant", tenantId]);
|
|
320
|
+
console.log("\u{1F4DD} \u2705 Example todos seeded\n");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (stressTestEnabled) {
|
|
324
|
+
console.log(
|
|
325
|
+
`\u{1F3CB}\uFE0F Seeding stress test customers${stressTestLite ? " (lite payload)" : ""}...`
|
|
326
|
+
);
|
|
327
|
+
const stressArgs = ["--tenant", tenantId, "--org", orgId, "--count", String(stressTestCount)];
|
|
328
|
+
if (stressTestLite) stressArgs.push("--lite");
|
|
329
|
+
await runModuleCommand(allModules, "customers", "seed-stresstest", stressArgs);
|
|
330
|
+
console.log(`\u2705 Stress test customers seeded (requested ${stressTestCount})
|
|
331
|
+
`);
|
|
332
|
+
}
|
|
333
|
+
console.log("\u{1F9E9} Enabling default dashboard widgets...");
|
|
334
|
+
await runModuleCommand(allModules, "dashboards", "seed-defaults", ["--tenant", tenantId]);
|
|
335
|
+
console.log("\u2705 Dashboard widgets enabled\n");
|
|
336
|
+
} else {
|
|
337
|
+
console.log("\u26A0\uFE0F Could not get organization ID or tenant ID, skipping seeding steps\n");
|
|
338
|
+
}
|
|
339
|
+
console.log("\u{1F9E0} Building search indexes...");
|
|
340
|
+
const vectorArgs = tenantId ? ["--tenant", tenantId, ...orgId ? ["--org", orgId] : []] : ["--purgeFirst=false"];
|
|
341
|
+
await runModuleCommand(allModules, "search", "reindex", vectorArgs);
|
|
342
|
+
console.log("\u2705 Search indexes built\n");
|
|
343
|
+
console.log("\u{1F50D} Rebuilding query indexes...");
|
|
344
|
+
const queryIndexArgs = ["--force", ...tenantId ? ["--tenant", tenantId] : []];
|
|
345
|
+
await runModuleCommand(allModules, "query_index", "reindex", queryIndexArgs);
|
|
346
|
+
console.log("\u2705 Query indexes rebuilt\n");
|
|
347
|
+
const [local, domain] = String(email).split("@");
|
|
348
|
+
const isSuperadminLocal = (local || "").toLowerCase() === "superadmin" && !!domain;
|
|
349
|
+
const adminEmailDerived = isSuperadminLocal ? `admin@${domain}` : null;
|
|
350
|
+
const employeeEmailDerived = isSuperadminLocal ? `employee@${domain}` : null;
|
|
351
|
+
console.log("\u{1F389} App initialization complete!\n");
|
|
352
|
+
console.log("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
353
|
+
console.log("\u2551 \u{1F680} You're now ready to start development! \u2551");
|
|
354
|
+
console.log("\u2551 \u2551");
|
|
355
|
+
console.log("\u2551 Start the dev server: \u2551");
|
|
356
|
+
console.log("\u2551 yarn dev \u2551");
|
|
357
|
+
console.log("\u2551 \u2551");
|
|
358
|
+
console.log("\u2551 Users created: \u2551");
|
|
359
|
+
console.log(`\u2551 \u{1F451} Superadmin: ${email.padEnd(42)} \u2551`);
|
|
360
|
+
console.log(`\u2551 Password: ${password.padEnd(44)} \u2551`);
|
|
361
|
+
if (adminEmailDerived) {
|
|
362
|
+
console.log(`\u2551 \u{1F9F0} Admin: ${adminEmailDerived.padEnd(42)} \u2551`);
|
|
363
|
+
console.log(`\u2551 Password: ${password.padEnd(44)} \u2551`);
|
|
364
|
+
}
|
|
365
|
+
if (employeeEmailDerived) {
|
|
366
|
+
console.log(`\u2551 \u{1F477} Employee: ${employeeEmailDerived.padEnd(42)} \u2551`);
|
|
367
|
+
console.log(`\u2551 Password: ${password.padEnd(44)} \u2551`);
|
|
368
|
+
}
|
|
369
|
+
console.log("\u2551 \u2551");
|
|
370
|
+
console.log("\u2551 Happy coding! \u2551");
|
|
371
|
+
console.log("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
372
|
+
return 0;
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (error instanceof Error) {
|
|
375
|
+
console.error("\u274C Initialization failed:", error.message);
|
|
376
|
+
} else {
|
|
377
|
+
console.error("\u274C Initialization failed:", error);
|
|
378
|
+
}
|
|
379
|
+
return 1;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
let modName = first;
|
|
383
|
+
let cmdName = second;
|
|
384
|
+
let rest = remaining;
|
|
385
|
+
if (first === "reindex") {
|
|
386
|
+
modName = "query_index";
|
|
387
|
+
cmdName = "reindex";
|
|
388
|
+
rest = second !== void 0 ? [second, ...remaining] : remaining;
|
|
389
|
+
}
|
|
390
|
+
if (first === "generate" && !second) {
|
|
391
|
+
cmdName = "all";
|
|
392
|
+
rest = remaining;
|
|
393
|
+
}
|
|
394
|
+
const modules = getCliModules();
|
|
395
|
+
let appCli = [];
|
|
396
|
+
try {
|
|
397
|
+
const dynImport = Function("return import")();
|
|
398
|
+
const app = await dynImport.then((f) => f("@/cli")).catch(() => null);
|
|
399
|
+
if (app && Array.isArray(app?.default)) appCli = app.default;
|
|
400
|
+
} catch {
|
|
401
|
+
}
|
|
402
|
+
const all = modules.slice();
|
|
403
|
+
all.push({
|
|
404
|
+
id: "queue",
|
|
405
|
+
cli: [
|
|
406
|
+
{
|
|
407
|
+
command: "worker",
|
|
408
|
+
run: async (args) => {
|
|
409
|
+
const isAllQueues = args.includes("--all");
|
|
410
|
+
const queueName = isAllQueues ? null : args[0];
|
|
411
|
+
const allWorkers = [];
|
|
412
|
+
for (const mod2 of getCliModules()) {
|
|
413
|
+
const modWorkers = mod2.workers;
|
|
414
|
+
if (modWorkers) {
|
|
415
|
+
allWorkers.push(...modWorkers);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const discoveredQueues = [...new Set(allWorkers.map((w) => w.queue))];
|
|
419
|
+
if (!queueName && !isAllQueues) {
|
|
420
|
+
console.error("Usage: mercato queue worker <queueName> | --all");
|
|
421
|
+
console.error("Example: mercato queue worker events");
|
|
422
|
+
console.error("Example: mercato queue worker --all");
|
|
423
|
+
if (discoveredQueues.length > 0) {
|
|
424
|
+
console.error(`Discovered queues: ${discoveredQueues.join(", ")}`);
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const concurrencyArg = args.find((a) => a.startsWith("--concurrency="));
|
|
429
|
+
const concurrencyOverride = concurrencyArg ? Number(concurrencyArg.split("=")[1]) : void 0;
|
|
430
|
+
if (isAllQueues) {
|
|
431
|
+
if (discoveredQueues.length === 0) {
|
|
432
|
+
console.error("[worker] No queues discovered from modules");
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const container = await createRequestContainer();
|
|
436
|
+
console.log(`[worker] Starting workers for all queues: ${discoveredQueues.join(", ")}`);
|
|
437
|
+
const workerPromises = discoveredQueues.map(async (queue) => {
|
|
438
|
+
const queueWorkers = allWorkers.filter((w) => w.queue === queue);
|
|
439
|
+
const concurrency = concurrencyOverride ?? Math.max(...queueWorkers.map((w) => w.concurrency), 1);
|
|
440
|
+
console.log(`[worker] Starting "${queue}" with ${queueWorkers.length} handler(s), concurrency: ${concurrency}`);
|
|
441
|
+
await runWorker({
|
|
442
|
+
queueName: queue,
|
|
443
|
+
connection: { url: process.env.REDIS_URL || process.env.QUEUE_REDIS_URL },
|
|
444
|
+
concurrency,
|
|
445
|
+
background: true,
|
|
446
|
+
handler: async (job, ctx) => {
|
|
447
|
+
for (const worker of queueWorkers) {
|
|
448
|
+
await worker.handler(job, { ...ctx, resolve: container.resolve.bind(container) });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
await Promise.all(workerPromises);
|
|
454
|
+
console.log("[worker] All workers started. Press Ctrl+C to stop");
|
|
455
|
+
await new Promise(() => {
|
|
456
|
+
});
|
|
457
|
+
} else {
|
|
458
|
+
const queueWorkers = allWorkers.filter((w) => w.queue === queueName);
|
|
459
|
+
if (queueWorkers.length > 0) {
|
|
460
|
+
const container = await createRequestContainer();
|
|
461
|
+
const concurrency = concurrencyOverride ?? Math.max(...queueWorkers.map((w) => w.concurrency), 1);
|
|
462
|
+
console.log(`[worker] Found ${queueWorkers.length} worker(s) for queue "${queueName}"`);
|
|
463
|
+
await runWorker({
|
|
464
|
+
queueName,
|
|
465
|
+
connection: { url: process.env.REDIS_URL || process.env.QUEUE_REDIS_URL },
|
|
466
|
+
concurrency,
|
|
467
|
+
handler: async (job, ctx) => {
|
|
468
|
+
for (const worker of queueWorkers) {
|
|
469
|
+
await worker.handler(job, { ...ctx, resolve: container.resolve.bind(container) });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
} else {
|
|
474
|
+
console.error(`No workers found for queue "${queueName}"`);
|
|
475
|
+
if (discoveredQueues.length > 0) {
|
|
476
|
+
console.error(`Available queues: ${discoveredQueues.join(", ")}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
command: "clear",
|
|
484
|
+
run: async (args) => {
|
|
485
|
+
const queueName = args[0];
|
|
486
|
+
if (!queueName) {
|
|
487
|
+
console.error("Usage: mercato queue clear <queueName>");
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const strategyEnv = process.env.QUEUE_STRATEGY || "local";
|
|
491
|
+
const { createQueue } = await import("@open-mercato/queue");
|
|
492
|
+
const queue = strategyEnv === "async" ? createQueue(queueName, "async", {
|
|
493
|
+
connection: { url: process.env.REDIS_URL || process.env.QUEUE_REDIS_URL }
|
|
494
|
+
}) : createQueue(queueName, "local");
|
|
495
|
+
const res = await queue.clear();
|
|
496
|
+
await queue.close();
|
|
497
|
+
console.log(`Cleared queue "${queueName}", removed ${res.removed} jobs`);
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
command: "status",
|
|
502
|
+
run: async (args) => {
|
|
503
|
+
const queueName = args[0];
|
|
504
|
+
if (!queueName) {
|
|
505
|
+
console.error("Usage: mercato queue status <queueName>");
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const strategyEnv = process.env.QUEUE_STRATEGY || "local";
|
|
509
|
+
const { createQueue } = await import("@open-mercato/queue");
|
|
510
|
+
const queue = strategyEnv === "async" ? createQueue(queueName, "async", {
|
|
511
|
+
connection: { url: process.env.REDIS_URL || process.env.QUEUE_REDIS_URL }
|
|
512
|
+
}) : createQueue(queueName, "local");
|
|
513
|
+
const counts = await queue.getJobCounts();
|
|
514
|
+
console.log(`Queue "${queueName}" status:`);
|
|
515
|
+
console.log(` Waiting: ${counts.waiting}`);
|
|
516
|
+
console.log(` Active: ${counts.active}`);
|
|
517
|
+
console.log(` Completed: ${counts.completed}`);
|
|
518
|
+
console.log(` Failed: ${counts.failed}`);
|
|
519
|
+
await queue.close();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
]
|
|
523
|
+
});
|
|
524
|
+
all.push({
|
|
525
|
+
id: "events",
|
|
526
|
+
cli: [
|
|
527
|
+
{
|
|
528
|
+
command: "emit",
|
|
529
|
+
run: async (args) => {
|
|
530
|
+
const eventName = args[0];
|
|
531
|
+
if (!eventName) {
|
|
532
|
+
console.error("Usage: mercato events emit <event> [jsonPayload] [--persistent|-p]");
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const persistent = args.includes("--persistent") || args.includes("-p");
|
|
536
|
+
const payloadArg = args[1] && !args[1].startsWith("--") ? args[1] : void 0;
|
|
537
|
+
let payload = {};
|
|
538
|
+
if (payloadArg) {
|
|
539
|
+
try {
|
|
540
|
+
payload = JSON.parse(payloadArg);
|
|
541
|
+
} catch {
|
|
542
|
+
payload = payloadArg;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const { createRequestContainer: createRequestContainer2 } = await import("@open-mercato/shared/lib/di/container");
|
|
546
|
+
const container = await createRequestContainer2();
|
|
547
|
+
const bus = container.resolve("eventBus");
|
|
548
|
+
await bus.emit(eventName, payload, { persistent });
|
|
549
|
+
console.log(`Emitted "${eventName}"${persistent ? " (persistent)" : ""}`);
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
command: "clear",
|
|
554
|
+
run: async () => {
|
|
555
|
+
const { createRequestContainer: createRequestContainer2 } = await import("@open-mercato/shared/lib/di/container");
|
|
556
|
+
const container = await createRequestContainer2();
|
|
557
|
+
const bus = container.resolve("eventBus");
|
|
558
|
+
const res = await bus.clearQueue();
|
|
559
|
+
console.log(`Cleared events queue, removed ${res.removed} events`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
]
|
|
563
|
+
});
|
|
564
|
+
all.push({
|
|
565
|
+
id: "scaffold",
|
|
566
|
+
cli: [
|
|
567
|
+
{
|
|
568
|
+
command: "module",
|
|
569
|
+
run: async (args) => {
|
|
570
|
+
const name = (args[0] || "").trim();
|
|
571
|
+
if (!name) {
|
|
572
|
+
console.error("Usage: mercato scaffold module <name>");
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const fs2 = await import("node:fs");
|
|
576
|
+
const path2 = await import("node:path");
|
|
577
|
+
const { execSync } = await import("node:child_process");
|
|
578
|
+
const base = path2.resolve("src/modules", name);
|
|
579
|
+
const folders = ["api", "backend", "frontend", "data", "subscribers"];
|
|
580
|
+
for (const f of folders) fs2.mkdirSync(path2.join(base, f), { recursive: true });
|
|
581
|
+
const moduleTitle = `${name[0].toUpperCase()}${name.slice(1)}`;
|
|
582
|
+
const indexTs = `export const metadata = { title: '${moduleTitle}', group: 'Modules' }
|
|
583
|
+
`;
|
|
584
|
+
fs2.writeFileSync(path2.join(base, "index.ts"), indexTs, { flag: "wx" });
|
|
585
|
+
const ceTs = `export const entities = [
|
|
586
|
+
{
|
|
587
|
+
id: '${name}:sample',
|
|
588
|
+
label: '${moduleTitle} Sample',
|
|
589
|
+
description: 'Describe your custom entity',
|
|
590
|
+
showInSidebar: true,
|
|
591
|
+
fields: [
|
|
592
|
+
// { key: 'priority', kind: 'integer', label: 'Priority' },
|
|
593
|
+
],
|
|
594
|
+
},
|
|
595
|
+
]
|
|
596
|
+
|
|
597
|
+
export default entities
|
|
598
|
+
`;
|
|
599
|
+
fs2.writeFileSync(path2.join(base, "ce.ts"), ceTs, { flag: "wx" });
|
|
600
|
+
const entitiesTs = `import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
|
|
601
|
+
|
|
602
|
+
// Add your entities here. Example:
|
|
603
|
+
// @Entity({ tableName: '${name}_items' })
|
|
604
|
+
// export class ${moduleTitle}Item {
|
|
605
|
+
// @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string
|
|
606
|
+
// @Property({ type: 'text' }) title!: string
|
|
607
|
+
// @Property({ name: 'organization_id', type: 'uuid', nullable: true }) organizationId?: string | null
|
|
608
|
+
// @Property({ name: 'tenant_id', type: 'uuid', nullable: true }) tenantId?: string | null
|
|
609
|
+
// @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date()
|
|
610
|
+
// @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() }) updatedAt: Date = new Date()
|
|
611
|
+
// @Property({ name: 'deleted_at', type: Date, nullable: true }) deletedAt?: Date | null
|
|
612
|
+
// }
|
|
613
|
+
`;
|
|
614
|
+
fs2.writeFileSync(path2.join(base, "data", "entities.ts"), entitiesTs, { flag: "wx" });
|
|
615
|
+
console.log(`Created module at ${path2.relative(process.cwd(), base)}`);
|
|
616
|
+
execSync("yarn modules:prepare", { stdio: "inherit" });
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
command: "entity",
|
|
621
|
+
run: async () => {
|
|
622
|
+
const fs2 = await import("node:fs");
|
|
623
|
+
const path2 = await import("node:path");
|
|
624
|
+
const readline = await import("node:readline/promises");
|
|
625
|
+
const { stdin: input, stdout: output } = await import("node:process");
|
|
626
|
+
const { execSync } = await import("node:child_process");
|
|
627
|
+
const rl = readline.createInterface({ input, output });
|
|
628
|
+
try {
|
|
629
|
+
const moduleId = (await rl.question("Module id (folder under src/modules): ")).trim();
|
|
630
|
+
const className = (await rl.question("Entity class name (e.g., Todo): ")).trim();
|
|
631
|
+
const tableName = (await rl.question(`DB table name (default: ${className.toLowerCase()}s): `)).trim() || `${className.toLowerCase()}s`;
|
|
632
|
+
const extra = (await rl.question("Additional fields (comma list name:type, e.g., title:text,is_done:boolean): ")).trim();
|
|
633
|
+
const extras = extra ? extra.split(",").map((s) => s.trim()).filter(Boolean).map((s) => {
|
|
634
|
+
const [n, t] = s.split(":").map((x) => x.trim());
|
|
635
|
+
return { n, t };
|
|
636
|
+
}) : [];
|
|
637
|
+
const base = path2.resolve("src/modules", moduleId, "data");
|
|
638
|
+
fs2.mkdirSync(base, { recursive: true });
|
|
639
|
+
const file = path2.join(base, "entities.ts");
|
|
640
|
+
let content = fs2.existsSync(file) ? fs2.readFileSync(file, "utf8") : `import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
|
|
641
|
+
|
|
642
|
+
`;
|
|
643
|
+
content += `
|
|
644
|
+
@Entity({ tableName: '${tableName}' })
|
|
645
|
+
export class ${className} {
|
|
646
|
+
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
647
|
+
id!: string
|
|
648
|
+
|
|
649
|
+
@Property({ name: 'organization_id', type: 'uuid', nullable: true })
|
|
650
|
+
organizationId?: string | null
|
|
651
|
+
|
|
652
|
+
@Property({ name: 'tenant_id', type: 'uuid', nullable: true })
|
|
653
|
+
tenantId?: string | null
|
|
654
|
+
|
|
655
|
+
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
656
|
+
createdAt: Date = new Date()
|
|
657
|
+
|
|
658
|
+
@Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
|
|
659
|
+
updatedAt: Date = new Date()
|
|
660
|
+
|
|
661
|
+
@Property({ name: 'deleted_at', type: Date, nullable: true })
|
|
662
|
+
deletedAt?: Date | null
|
|
663
|
+
`;
|
|
664
|
+
for (const f of extras) {
|
|
665
|
+
const n = f.n;
|
|
666
|
+
const t = f.t;
|
|
667
|
+
if (!n || !t) continue;
|
|
668
|
+
const map = {
|
|
669
|
+
text: { ts: "string", db: "text" },
|
|
670
|
+
multiline: { ts: "string", db: "text" },
|
|
671
|
+
integer: { ts: "number", db: "int" },
|
|
672
|
+
float: { ts: "number", db: "float" },
|
|
673
|
+
boolean: { ts: "boolean", db: "boolean" },
|
|
674
|
+
date: { ts: "Date", db: "Date" }
|
|
675
|
+
};
|
|
676
|
+
const info = map[t];
|
|
677
|
+
const fallback = { ts: "string", db: "text" };
|
|
678
|
+
const resolved = info || fallback;
|
|
679
|
+
const propName = n.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
680
|
+
const columnName = n.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`);
|
|
681
|
+
const dbType = resolved.db;
|
|
682
|
+
const tsType = resolved.ts;
|
|
683
|
+
const defaultValue = resolved.ts === "boolean" ? " = false" : resolved.ts === "Date" ? " = new Date()" : "";
|
|
684
|
+
content += `
|
|
685
|
+
@Property({ name: '${columnName}', type: ${dbType === "Date" ? "Date" : `'${dbType}'`}${resolved.ts === "boolean" ? ", default: false" : ""} })
|
|
686
|
+
${propName}${tsType === "number" ? "?: number | null" : tsType === "boolean" ? ": boolean" : tsType === "Date" ? ": Date" : "!: string"}${defaultValue}
|
|
687
|
+
`;
|
|
688
|
+
}
|
|
689
|
+
content += `}
|
|
690
|
+
`;
|
|
691
|
+
fs2.writeFileSync(file, content);
|
|
692
|
+
console.log(`Updated ${path2.relative(process.cwd(), file)}`);
|
|
693
|
+
console.log("Generating and applying migrations...");
|
|
694
|
+
execSync("yarn modules:prepare", { stdio: "inherit" });
|
|
695
|
+
execSync("yarn db:generate", { stdio: "inherit" });
|
|
696
|
+
execSync("yarn db:migrate", { stdio: "inherit" });
|
|
697
|
+
} finally {
|
|
698
|
+
rl.close();
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
command: "crud",
|
|
704
|
+
run: async (args) => {
|
|
705
|
+
const fs2 = await import("node:fs");
|
|
706
|
+
const path2 = await import("node:path");
|
|
707
|
+
const { execSync } = await import("node:child_process");
|
|
708
|
+
const mod2 = (args[0] || "").trim();
|
|
709
|
+
const entity = (args[1] || "").trim();
|
|
710
|
+
const routeSeg = (args[2] || "").trim() || `${entity.toLowerCase()}s`;
|
|
711
|
+
if (!mod2 || !entity) {
|
|
712
|
+
console.error("Usage: mercato scaffold crud <moduleId> <EntityClass> [routeSegment]");
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const baseDir = path2.resolve("src/modules", mod2, "api", routeSeg);
|
|
716
|
+
fs2.mkdirSync(baseDir, { recursive: true });
|
|
717
|
+
const entitySnake = entity.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
718
|
+
const tmpl = `import { z } from 'zod'
|
|
719
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
720
|
+
import { ${entity} } from '@open-mercato/shared/modules/${mod2}/data/entities'
|
|
721
|
+
import { E } from '#generated/entities.ids.generated'
|
|
722
|
+
import ceEntities from '@open-mercato/shared/modules/${mod2}/ce'
|
|
723
|
+
import { buildCustomFieldSelectorsForEntity, extractCustomFieldsFromItem, buildCustomFieldFiltersFromQuery } from '@open-mercato/shared/lib/crud/custom-fields'
|
|
724
|
+
import type { CustomFieldSet } from '@open-mercato/shared/modules/entities'
|
|
725
|
+
|
|
726
|
+
// Field constants - update these based on your entity's actual fields
|
|
727
|
+
const F = {
|
|
728
|
+
id: 'id',
|
|
729
|
+
tenant_id: 'tenant_id',
|
|
730
|
+
organization_id: 'organization_id',
|
|
731
|
+
created_at: 'created_at',
|
|
732
|
+
updated_at: 'updated_at',
|
|
733
|
+
deleted_at: 'deleted_at',
|
|
734
|
+
} as const
|
|
735
|
+
|
|
736
|
+
const querySchema = z.object({
|
|
737
|
+
id: z.string().uuid().optional(),
|
|
738
|
+
page: z.coerce.number().min(1).default(1),
|
|
739
|
+
pageSize: z.coerce.number().min(1).max(100).default(50),
|
|
740
|
+
sortField: z.string().optional().default('id'),
|
|
741
|
+
sortDir: z.enum(['asc','desc']).optional().default('asc'),
|
|
742
|
+
withDeleted: z.coerce.boolean().optional().default(false),
|
|
743
|
+
}).passthrough()
|
|
744
|
+
|
|
745
|
+
const createSchema = z.object({}).passthrough()
|
|
746
|
+
const updateSchema = z.object({ id: z.string().uuid() }).passthrough()
|
|
747
|
+
|
|
748
|
+
type Query = z.infer<typeof querySchema>
|
|
749
|
+
|
|
750
|
+
const fieldSets: CustomFieldSet[] = []
|
|
751
|
+
const ceEntity = Array.isArray(ceEntities) ? ceEntities.find((entity) => entity?.id === '${mod2}:${entitySnake}') : undefined
|
|
752
|
+
if (ceEntity?.fields?.length) {
|
|
753
|
+
fieldSets.push({ entity: ceEntity.id, fields: ceEntity.fields, source: '${mod2}' })
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const cfSel = buildCustomFieldSelectorsForEntity(E.${mod2}.${entitySnake}, fieldSets)
|
|
757
|
+
const sortFieldMap: Record<string, unknown> = { id: F.id, created_at: F.created_at, ...Object.fromEntries(cfSel.keys.map(k => [\`cf_\${k}\`, \`cf:\${k}\`])) }
|
|
758
|
+
|
|
759
|
+
export const { metadata, GET, POST, PUT, DELETE } = makeCrudRoute({
|
|
760
|
+
metadata: { GET: { requireAuth: true }, POST: { requireAuth: true }, PUT: { requireAuth: true }, DELETE: { requireAuth: true } },
|
|
761
|
+
orm: { entity: ${entity}, idField: 'id', orgField: 'organizationId', tenantField: 'tenantId', softDeleteField: 'deletedAt' },
|
|
762
|
+
events: { module: '${mod2}', entity: '${entitySnake}', persistent: true },
|
|
763
|
+
indexer: { entityType: E.${mod2}.${entitySnake} },
|
|
764
|
+
list: {
|
|
765
|
+
schema: querySchema,
|
|
766
|
+
entityId: E.${mod2}.${entitySnake},
|
|
767
|
+
fields: [F.id, F.created_at, ...cfSel.selectors],
|
|
768
|
+
sortFieldMap,
|
|
769
|
+
buildFilters: async (q: Query, ctx) => ({
|
|
770
|
+
...(await buildCustomFieldFiltersFromQuery({
|
|
771
|
+
entityId: E.${mod2}.${entitySnake},
|
|
772
|
+
query: q as any,
|
|
773
|
+
em: ctx.container.resolve('em'),
|
|
774
|
+
tenantId: ctx.auth!.tenantId,
|
|
775
|
+
})),
|
|
776
|
+
}),
|
|
777
|
+
transformItem: (item: any) => ({ id: item.id, created_at: item.created_at, ...extractCustomFieldsFromItem(item, cfSel.keys) }),
|
|
778
|
+
},
|
|
779
|
+
create: { schema: createSchema, mapToEntity: (input: any) => ({}), customFields: { enabled: true, entityId: E.${mod2}.${entitySnake}, pickPrefixed: true } },
|
|
780
|
+
update: { schema: updateSchema, applyToEntity: (entity: ${entity}, input: any) => {}, customFields: { enabled: true, entityId: E.${mod2}.${entitySnake}, pickPrefixed: true } },
|
|
781
|
+
del: { idFrom: 'query', softDelete: true },
|
|
782
|
+
})
|
|
783
|
+
`;
|
|
784
|
+
const file = path2.join(baseDir, "route.ts");
|
|
785
|
+
fs2.writeFileSync(file, tmpl, { flag: "wx" });
|
|
786
|
+
console.log(`Created CRUD route: ${path2.relative(process.cwd(), file)}`);
|
|
787
|
+
execSync("yarn modules:prepare", { stdio: "inherit" });
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
]
|
|
791
|
+
});
|
|
792
|
+
all.push({
|
|
793
|
+
id: "generate",
|
|
794
|
+
cli: [
|
|
795
|
+
{
|
|
796
|
+
command: "all",
|
|
797
|
+
run: async (args) => {
|
|
798
|
+
const { createResolver } = await import("./lib/resolver.js");
|
|
799
|
+
const { generateEntityIds, generateModuleRegistry, generateModuleRegistryCli, generateModuleEntities, generateModuleDi } = await import("./lib/generators/index.js");
|
|
800
|
+
const resolver = createResolver();
|
|
801
|
+
const quiet = args.includes("--quiet") || args.includes("-q");
|
|
802
|
+
console.log("Running all generators...");
|
|
803
|
+
await generateEntityIds({ resolver, quiet });
|
|
804
|
+
await generateModuleRegistry({ resolver, quiet });
|
|
805
|
+
await generateModuleRegistryCli({ resolver, quiet });
|
|
806
|
+
await generateModuleEntities({ resolver, quiet });
|
|
807
|
+
await generateModuleDi({ resolver, quiet });
|
|
808
|
+
console.log("All generators completed.");
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
command: "entity-ids",
|
|
813
|
+
run: async (args) => {
|
|
814
|
+
const { createResolver } = await import("./lib/resolver.js");
|
|
815
|
+
const { generateEntityIds } = await import("./lib/generators/index.js");
|
|
816
|
+
const resolver = createResolver();
|
|
817
|
+
await generateEntityIds({ resolver, quiet: args.includes("--quiet") });
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
command: "registry",
|
|
822
|
+
run: async (args) => {
|
|
823
|
+
const { createResolver } = await import("./lib/resolver.js");
|
|
824
|
+
const { generateModuleRegistry } = await import("./lib/generators/index.js");
|
|
825
|
+
const resolver = createResolver();
|
|
826
|
+
await generateModuleRegistry({ resolver, quiet: args.includes("--quiet") });
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
command: "entities",
|
|
831
|
+
run: async (args) => {
|
|
832
|
+
const { createResolver } = await import("./lib/resolver.js");
|
|
833
|
+
const { generateModuleEntities } = await import("./lib/generators/index.js");
|
|
834
|
+
const resolver = createResolver();
|
|
835
|
+
await generateModuleEntities({ resolver, quiet: args.includes("--quiet") });
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
command: "di",
|
|
840
|
+
run: async (args) => {
|
|
841
|
+
const { createResolver } = await import("./lib/resolver.js");
|
|
842
|
+
const { generateModuleDi } = await import("./lib/generators/index.js");
|
|
843
|
+
const resolver = createResolver();
|
|
844
|
+
await generateModuleDi({ resolver, quiet: args.includes("--quiet") });
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
]
|
|
848
|
+
});
|
|
849
|
+
all.push({
|
|
850
|
+
id: "db",
|
|
851
|
+
cli: [
|
|
852
|
+
{
|
|
853
|
+
command: "generate",
|
|
854
|
+
run: async () => {
|
|
855
|
+
const { createResolver } = await import("./lib/resolver.js");
|
|
856
|
+
const { dbGenerate } = await import("./lib/db/index.js");
|
|
857
|
+
const resolver = createResolver();
|
|
858
|
+
await dbGenerate(resolver);
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
command: "migrate",
|
|
863
|
+
run: async () => {
|
|
864
|
+
const { createResolver } = await import("./lib/resolver.js");
|
|
865
|
+
const { dbMigrate } = await import("./lib/db/index.js");
|
|
866
|
+
const resolver = createResolver();
|
|
867
|
+
await dbMigrate(resolver);
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
command: "greenfield",
|
|
872
|
+
run: async (args) => {
|
|
873
|
+
const { createResolver } = await import("./lib/resolver.js");
|
|
874
|
+
const { dbGreenfield } = await import("./lib/db/index.js");
|
|
875
|
+
const resolver = createResolver();
|
|
876
|
+
const yes = args.includes("--yes") || args.includes("-y");
|
|
877
|
+
await dbGreenfield(resolver, { yes });
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
]
|
|
881
|
+
});
|
|
882
|
+
all.push({
|
|
883
|
+
id: "server",
|
|
884
|
+
cli: [
|
|
885
|
+
{
|
|
886
|
+
command: "dev",
|
|
887
|
+
run: async () => {
|
|
888
|
+
const { spawn } = await import("child_process");
|
|
889
|
+
const path2 = await import("path");
|
|
890
|
+
const { createResolver } = await import("./lib/resolver.js");
|
|
891
|
+
const resolver = createResolver();
|
|
892
|
+
const appDir = resolver.getAppDir();
|
|
893
|
+
const nodeModulesBase = resolver.isMonorepo() ? resolver.getRootDir() : appDir;
|
|
894
|
+
const processes = [];
|
|
895
|
+
const autoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS !== "false";
|
|
896
|
+
function cleanup() {
|
|
897
|
+
console.log("[server] Shutting down...");
|
|
898
|
+
for (const proc of processes) {
|
|
899
|
+
if (!proc.killed) {
|
|
900
|
+
proc.kill("SIGTERM");
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
process.on("SIGTERM", cleanup);
|
|
905
|
+
process.on("SIGINT", cleanup);
|
|
906
|
+
console.log("[server] Starting Open Mercato in dev mode...");
|
|
907
|
+
const nextBin = path2.join(nodeModulesBase, "node_modules/next/dist/bin/next");
|
|
908
|
+
const mercatoBin = path2.join(nodeModulesBase, "node_modules/@open-mercato/cli/bin/mercato");
|
|
909
|
+
const nextProcess = spawn("node", [nextBin, "dev", "--turbopack"], {
|
|
910
|
+
stdio: "inherit",
|
|
911
|
+
env: process.env,
|
|
912
|
+
cwd: appDir
|
|
913
|
+
});
|
|
914
|
+
processes.push(nextProcess);
|
|
915
|
+
if (autoSpawnWorkers) {
|
|
916
|
+
console.log("[server] Starting workers for all queues...");
|
|
917
|
+
const workerProcess = spawn("node", [mercatoBin, "queue", "worker", "--all"], {
|
|
918
|
+
stdio: "inherit",
|
|
919
|
+
env: process.env,
|
|
920
|
+
cwd: appDir
|
|
921
|
+
});
|
|
922
|
+
processes.push(workerProcess);
|
|
923
|
+
}
|
|
924
|
+
await Promise.race(
|
|
925
|
+
processes.map(
|
|
926
|
+
(proc) => new Promise((resolve) => {
|
|
927
|
+
proc.on("exit", () => resolve());
|
|
928
|
+
})
|
|
929
|
+
)
|
|
930
|
+
);
|
|
931
|
+
cleanup();
|
|
932
|
+
}
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
command: "start",
|
|
936
|
+
run: async () => {
|
|
937
|
+
const { spawn } = await import("child_process");
|
|
938
|
+
const path2 = await import("path");
|
|
939
|
+
const { createResolver } = await import("./lib/resolver.js");
|
|
940
|
+
const resolver = createResolver();
|
|
941
|
+
const appDir = resolver.getAppDir();
|
|
942
|
+
const nodeModulesBase = resolver.isMonorepo() ? resolver.getRootDir() : appDir;
|
|
943
|
+
const processes = [];
|
|
944
|
+
const autoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS !== "false";
|
|
945
|
+
function cleanup() {
|
|
946
|
+
console.log("[server] Shutting down...");
|
|
947
|
+
for (const proc of processes) {
|
|
948
|
+
if (!proc.killed) {
|
|
949
|
+
proc.kill("SIGTERM");
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
process.on("SIGTERM", cleanup);
|
|
954
|
+
process.on("SIGINT", cleanup);
|
|
955
|
+
console.log("[server] Starting Open Mercato in production mode...");
|
|
956
|
+
const nextBin = path2.join(nodeModulesBase, "node_modules/next/dist/bin/next");
|
|
957
|
+
const mercatoBin = path2.join(nodeModulesBase, "node_modules/@open-mercato/cli/bin/mercato");
|
|
958
|
+
const nextProcess = spawn("node", [nextBin, "start"], {
|
|
959
|
+
stdio: "inherit",
|
|
960
|
+
env: process.env,
|
|
961
|
+
cwd: appDir
|
|
962
|
+
});
|
|
963
|
+
processes.push(nextProcess);
|
|
964
|
+
if (autoSpawnWorkers) {
|
|
965
|
+
console.log("[server] Starting workers for all queues...");
|
|
966
|
+
const workerProcess = spawn("node", [mercatoBin, "queue", "worker", "--all"], {
|
|
967
|
+
stdio: "inherit",
|
|
968
|
+
env: process.env,
|
|
969
|
+
cwd: appDir
|
|
970
|
+
});
|
|
971
|
+
processes.push(workerProcess);
|
|
972
|
+
}
|
|
973
|
+
await Promise.race(
|
|
974
|
+
processes.map(
|
|
975
|
+
(proc) => new Promise((resolve) => {
|
|
976
|
+
proc.on("exit", () => resolve());
|
|
977
|
+
})
|
|
978
|
+
)
|
|
979
|
+
);
|
|
980
|
+
cleanup();
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
]
|
|
984
|
+
});
|
|
985
|
+
if (appCli.length) all.push({ id: "app", cli: appCli });
|
|
986
|
+
const quietBanner = process.env.OM_CLI_QUIET === "1";
|
|
987
|
+
const banner = "\u{1F9E9} Open Mercato CLI";
|
|
988
|
+
if (!quietBanner) {
|
|
989
|
+
const header = [
|
|
990
|
+
"\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
|
|
991
|
+
`\u2551 ${banner.padEnd(21)}\u2551`,
|
|
992
|
+
"\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"
|
|
993
|
+
].join("\n");
|
|
994
|
+
console.log(header);
|
|
995
|
+
}
|
|
996
|
+
const pad = (s) => ` ${s}`;
|
|
997
|
+
if (!modName || modName === "help" || modName === "--help" || modName === "-h") {
|
|
998
|
+
console.log(pad("Usage: \u2728 mercato <module> <command> [args]"));
|
|
999
|
+
const list = all.filter((m) => m.cli && m.cli.length).map((m) => `\u2022 ${m.id}: ${m.cli.map((c) => `"${c.command}"`).join(", ")}`);
|
|
1000
|
+
if (list.length) {
|
|
1001
|
+
console.log("\n" + pad("Available:"));
|
|
1002
|
+
console.log(list.map(pad).join("\n"));
|
|
1003
|
+
} else {
|
|
1004
|
+
console.log(pad("\u{1F300} No CLI commands available"));
|
|
1005
|
+
}
|
|
1006
|
+
return 0;
|
|
1007
|
+
}
|
|
1008
|
+
const mod = all.find((m) => m.id === modName);
|
|
1009
|
+
if (!mod) {
|
|
1010
|
+
console.error(`\u274C Module not found: "${modName}"`);
|
|
1011
|
+
return 1;
|
|
1012
|
+
}
|
|
1013
|
+
if (!mod.cli || mod.cli.length === 0) {
|
|
1014
|
+
console.error(`\u{1F6AB} Module "${modName}" has no CLI commands`);
|
|
1015
|
+
return 1;
|
|
1016
|
+
}
|
|
1017
|
+
if (!cmdName) {
|
|
1018
|
+
console.log(pad(`Commands for "${modName}": ${mod.cli.map((c) => c.command).join(", ")}`));
|
|
1019
|
+
return 1;
|
|
1020
|
+
}
|
|
1021
|
+
const cmd = mod.cli.find((c) => c.command === cmdName);
|
|
1022
|
+
if (!cmd) {
|
|
1023
|
+
console.error(`\u{1F914} Unknown command "${cmdName}". Available: ${mod.cli.map((c) => c.command).join(", ")}`);
|
|
1024
|
+
return 1;
|
|
1025
|
+
}
|
|
1026
|
+
console.log("");
|
|
1027
|
+
const started = Date.now();
|
|
1028
|
+
console.log(`\u{1F680} Running ${modName}:${cmdName} ${rest.join(" ")}`);
|
|
1029
|
+
try {
|
|
1030
|
+
await cmd.run(rest);
|
|
1031
|
+
const ms = Date.now() - started;
|
|
1032
|
+
console.log(`\u23F1\uFE0F Done in ${ms}ms`);
|
|
1033
|
+
return 0;
|
|
1034
|
+
} catch (e) {
|
|
1035
|
+
console.error(`\u{1F4A5} Failed: ${e?.message || e}`);
|
|
1036
|
+
return 1;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
export {
|
|
1040
|
+
getCliModules,
|
|
1041
|
+
hasCliModules,
|
|
1042
|
+
registerCliModules,
|
|
1043
|
+
run
|
|
1044
|
+
};
|
|
1045
|
+
//# sourceMappingURL=mercato.js.map
|