@open-mercato/shared 0.7.1-develop.7193.1.910a5b0a1e → 0.7.1-develop.7194.1.ab4fc81f82
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/.turbo/turbo-build.log +1 -1
- package/dist/lib/ai/opencode-tool-parts.js +48 -0
- package/dist/lib/ai/opencode-tool-parts.js.map +7 -0
- package/dist/lib/ai/token-count.js +11 -0
- package/dist/lib/ai/token-count.js.map +7 -0
- package/dist/lib/bootstrap/dynamicLoader.js +14 -1
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/commands/command-bus.js +9 -1
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/commands/registry.js +9 -0
- package/dist/lib/commands/registry.js.map +2 -2
- package/dist/lib/commands/types.js.map +2 -2
- package/dist/lib/openapi/generator.js +3 -2
- package/dist/lib/openapi/generator.js.map +2 -2
- package/dist/lib/openapi/index.js +3 -2
- package/dist/lib/openapi/index.js.map +2 -2
- package/dist/lib/seed/crypto.js +73 -0
- package/dist/lib/seed/crypto.js.map +7 -0
- package/dist/lib/seed/index.js +4 -0
- package/dist/lib/seed/index.js.map +7 -0
- package/dist/lib/seed/loader.js +73 -0
- package/dist/lib/seed/loader.js.map +7 -0
- package/dist/lib/seed/types.js +33 -0
- package/dist/lib/seed/types.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/events/factory.js +28 -9
- package/dist/modules/events/factory.js.map +2 -2
- package/package.json +3 -2
- package/src/lib/ai/__tests__/opencode-tool-parts.test.ts +81 -0
- package/src/lib/ai/__tests__/token-count.test.ts +20 -0
- package/src/lib/ai/opencode-tool-parts.ts +80 -0
- package/src/lib/ai/token-count.ts +21 -0
- package/src/lib/bootstrap/dynamicLoader.ts +24 -1
- package/src/lib/commands/__tests__/command-bus.test.ts +64 -0
- package/src/lib/commands/__tests__/registry.test.ts +35 -0
- package/src/lib/commands/command-bus.ts +16 -1
- package/src/lib/commands/registry.ts +11 -0
- package/src/lib/commands/types.ts +31 -0
- package/src/lib/openapi/__tests__/generator-response-fallback.test.ts +73 -0
- package/src/lib/openapi/generator.ts +3 -3
- package/src/lib/openapi/index.ts +1 -1
- package/src/lib/seed/__tests__/seed-crypto.test.ts +64 -0
- package/src/lib/seed/crypto.ts +87 -0
- package/src/lib/seed/index.ts +3 -0
- package/src/lib/seed/loader.ts +124 -0
- package/src/lib/seed/types.ts +48 -0
- package/src/modules/events/__tests__/factory.test.ts +96 -0
- package/src/modules/events/factory.ts +41 -10
- package/src/modules/events/types.ts +44 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { resolveEntityIdFromMetadata } from "../encryption/entityIds.js";
|
|
2
|
+
import { seedDocumentSchema } from "./types.js";
|
|
3
|
+
class SeedDryRunRollback extends Error {
|
|
4
|
+
}
|
|
5
|
+
function resolveEntityClass(meta) {
|
|
6
|
+
return meta.class ?? meta.className ?? meta.name;
|
|
7
|
+
}
|
|
8
|
+
function buildEntityIdIndex(em) {
|
|
9
|
+
const storage = em.getMetadata();
|
|
10
|
+
const all = (typeof storage.getAll === "function" ? storage.getAll() : storage.metadata) ?? {};
|
|
11
|
+
const list = all instanceof Map ? [...all.values()] : Array.isArray(all) ? all : Object.values(all);
|
|
12
|
+
const index = /* @__PURE__ */ new Map();
|
|
13
|
+
for (const meta of list) {
|
|
14
|
+
if (!meta || meta.abstract) continue;
|
|
15
|
+
const entityId = resolveEntityIdFromMetadata(meta);
|
|
16
|
+
if (!entityId) continue;
|
|
17
|
+
if (!index.has(entityId)) index.set(entityId, meta);
|
|
18
|
+
}
|
|
19
|
+
return index;
|
|
20
|
+
}
|
|
21
|
+
function hasProperty(meta, name) {
|
|
22
|
+
return Boolean(meta.properties && meta.properties[name]);
|
|
23
|
+
}
|
|
24
|
+
async function loadSeedDocument(em, document, scope, options = {}) {
|
|
25
|
+
const doc = seedDocumentSchema.parse(document);
|
|
26
|
+
const index = buildEntityIdIndex(em);
|
|
27
|
+
const total = doc.records.length;
|
|
28
|
+
let created = 0;
|
|
29
|
+
let skipped = 0;
|
|
30
|
+
const run = async (tem) => {
|
|
31
|
+
for (let i = 0; i < doc.records.length; i += 1) {
|
|
32
|
+
const record = doc.records[i];
|
|
33
|
+
const meta = index.get(record.entity);
|
|
34
|
+
if (!meta) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`[internal] Unknown seed entity "${record.entity}" at record ${i}: not a registered entity id.`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const entityClass = resolveEntityClass(meta);
|
|
40
|
+
const data = { ...record.data };
|
|
41
|
+
if (hasProperty(meta, "tenantId")) data.tenantId = scope.tenantId;
|
|
42
|
+
if (hasProperty(meta, "organizationId")) data.organizationId = scope.organizationId;
|
|
43
|
+
if (record.match && record.match.length) {
|
|
44
|
+
const where = {};
|
|
45
|
+
for (const field of record.match) where[field] = data[field];
|
|
46
|
+
if (hasProperty(meta, "tenantId")) where.tenantId = scope.tenantId;
|
|
47
|
+
if (hasProperty(meta, "organizationId")) where.organizationId = scope.organizationId;
|
|
48
|
+
const existing = await tem.findOne(entityClass, where);
|
|
49
|
+
if (existing) {
|
|
50
|
+
skipped += 1;
|
|
51
|
+
options.onProgress?.({ index: i, total, entity: record.entity, action: "skipped" });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const entity = tem.create(entityClass, data);
|
|
56
|
+
tem.persist(entity);
|
|
57
|
+
await tem.flush();
|
|
58
|
+
created += 1;
|
|
59
|
+
options.onProgress?.({ index: i, total, entity: record.entity, action: "created" });
|
|
60
|
+
}
|
|
61
|
+
if (options.dryRun) throw new SeedDryRunRollback();
|
|
62
|
+
};
|
|
63
|
+
try {
|
|
64
|
+
await em.transactional(run);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (!(err instanceof SeedDryRunRollback)) throw err;
|
|
67
|
+
}
|
|
68
|
+
return { total, created, skipped };
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
loadSeedDocument
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/seed/loader.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager, EntityMetadata } from '@mikro-orm/postgresql'\nimport { resolveEntityIdFromMetadata } from '../encryption/entityIds'\nimport { seedDocumentSchema, type SeedDocument } from './types'\n\nexport type SeedLoadScope = {\n tenantId: string\n organizationId: string\n}\n\nexport type SeedLoadProgress = {\n index: number\n total: number\n entity: string\n action: 'created' | 'skipped'\n}\n\nexport type SeedLoadOptions = {\n /** Apply inside a transaction and roll it back; reports what would happen. */\n dryRun?: boolean\n onProgress?: (progress: SeedLoadProgress) => void\n}\n\nexport type SeedLoadResult = {\n total: number\n created: number\n skipped: number\n}\n\nclass SeedDryRunRollback extends Error {}\n\nfunction resolveEntityClass(meta: EntityMetadata<any>): unknown {\n return (meta as any).class ?? meta.className ?? meta.name\n}\n\nfunction buildEntityIdIndex(em: EntityManager): Map<string, EntityMetadata<any>> {\n const storage = em.getMetadata() as unknown as {\n getAll?: () =>\n | Map<unknown, EntityMetadata<any>>\n | Record<string, EntityMetadata<any>>\n | EntityMetadata<any>[]\n metadata?: Record<string, EntityMetadata<any>>\n }\n const all = (typeof storage.getAll === 'function' ? storage.getAll() : storage.metadata) ?? {}\n // MikroORM v7's instance getAll() returns a Map; older shapes returned a plain\n // object or array. Normalize all three to a flat list.\n const list: EntityMetadata<any>[] =\n all instanceof Map ? [...all.values()] : Array.isArray(all) ? all : Object.values(all)\n const index = new Map<string, EntityMetadata<any>>()\n for (const meta of list) {\n if (!meta || (meta as any).abstract) continue\n const entityId = resolveEntityIdFromMetadata(meta)\n if (!entityId) continue\n if (!index.has(entityId)) index.set(entityId, meta)\n }\n return index\n}\n\nfunction hasProperty(meta: EntityMetadata<any>, name: string): boolean {\n return Boolean(meta.properties && (meta.properties as Record<string, unknown>)[name])\n}\n\n/**\n * Insert seed records through the ORM so the tenant-data-encryption subscriber\n * encrypts marked fields at rest automatically. Records are applied in order;\n * `tenantId`/`organizationId` are injected from `scope` for every entity that\n * declares them. Records with a `match` list are skipped when an existing row\n * matches (idempotent re-runs); match fields MUST be non-encrypted natural keys.\n */\nexport async function loadSeedDocument(\n em: EntityManager,\n document: SeedDocument,\n scope: SeedLoadScope,\n options: SeedLoadOptions = {},\n): Promise<SeedLoadResult> {\n const doc = seedDocumentSchema.parse(document)\n const index = buildEntityIdIndex(em)\n const total = doc.records.length\n let created = 0\n let skipped = 0\n\n const run = async (tem: EntityManager) => {\n for (let i = 0; i < doc.records.length; i += 1) {\n const record = doc.records[i]\n const meta = index.get(record.entity)\n if (!meta) {\n throw new Error(\n `[internal] Unknown seed entity \"${record.entity}\" at record ${i}: not a registered entity id.`,\n )\n }\n const entityClass = resolveEntityClass(meta)\n const data: Record<string, unknown> = { ...record.data }\n if (hasProperty(meta, 'tenantId')) data.tenantId = scope.tenantId\n if (hasProperty(meta, 'organizationId')) data.organizationId = scope.organizationId\n\n if (record.match && record.match.length) {\n const where: Record<string, unknown> = {}\n for (const field of record.match) where[field] = data[field]\n if (hasProperty(meta, 'tenantId')) where.tenantId = scope.tenantId\n if (hasProperty(meta, 'organizationId')) where.organizationId = scope.organizationId\n const existing = await tem.findOne(entityClass as any, where as any)\n if (existing) {\n skipped += 1\n options.onProgress?.({ index: i, total, entity: record.entity, action: 'skipped' })\n continue\n }\n }\n\n const entity = tem.create(entityClass as any, data as any)\n tem.persist(entity)\n await tem.flush()\n created += 1\n options.onProgress?.({ index: i, total, entity: record.entity, action: 'created' })\n }\n if (options.dryRun) throw new SeedDryRunRollback()\n }\n\n try {\n await em.transactional(run)\n } catch (err) {\n if (!(err instanceof SeedDryRunRollback)) throw err\n }\n\n return { total, created, skipped }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,mCAAmC;AAC5C,SAAS,0BAA6C;AA0BtD,MAAM,2BAA2B,MAAM;AAAC;AAExC,SAAS,mBAAmB,MAAoC;AAC9D,SAAQ,KAAa,SAAS,KAAK,aAAa,KAAK;AACvD;AAEA,SAAS,mBAAmB,IAAqD;AAC/E,QAAM,UAAU,GAAG,YAAY;AAO/B,QAAM,OAAO,OAAO,QAAQ,WAAW,aAAa,QAAQ,OAAO,IAAI,QAAQ,aAAa,CAAC;AAG7F,QAAM,OACJ,eAAe,MAAM,CAAC,GAAG,IAAI,OAAO,CAAC,IAAI,MAAM,QAAQ,GAAG,IAAI,MAAM,OAAO,OAAO,GAAG;AACvF,QAAM,QAAQ,oBAAI,IAAiC;AACnD,aAAW,QAAQ,MAAM;AACvB,QAAI,CAAC,QAAS,KAAa,SAAU;AACrC,UAAM,WAAW,4BAA4B,IAAI;AACjD,QAAI,CAAC,SAAU;AACf,QAAI,CAAC,MAAM,IAAI,QAAQ,EAAG,OAAM,IAAI,UAAU,IAAI;AAAA,EACpD;AACA,SAAO;AACT;AAEA,SAAS,YAAY,MAA2B,MAAuB;AACrE,SAAO,QAAQ,KAAK,cAAe,KAAK,WAAuC,IAAI,CAAC;AACtF;AASA,eAAsB,iBACpB,IACA,UACA,OACA,UAA2B,CAAC,GACH;AACzB,QAAM,MAAM,mBAAmB,MAAM,QAAQ;AAC7C,QAAM,QAAQ,mBAAmB,EAAE;AACnC,QAAM,QAAQ,IAAI,QAAQ;AAC1B,MAAI,UAAU;AACd,MAAI,UAAU;AAEd,QAAM,MAAM,OAAO,QAAuB;AACxC,aAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,QAAQ,KAAK,GAAG;AAC9C,YAAM,SAAS,IAAI,QAAQ,CAAC;AAC5B,YAAM,OAAO,MAAM,IAAI,OAAO,MAAM;AACpC,UAAI,CAAC,MAAM;AACT,cAAM,IAAI;AAAA,UACR,mCAAmC,OAAO,MAAM,eAAe,CAAC;AAAA,QAClE;AAAA,MACF;AACA,YAAM,cAAc,mBAAmB,IAAI;AAC3C,YAAM,OAAgC,EAAE,GAAG,OAAO,KAAK;AACvD,UAAI,YAAY,MAAM,UAAU,EAAG,MAAK,WAAW,MAAM;AACzD,UAAI,YAAY,MAAM,gBAAgB,EAAG,MAAK,iBAAiB,MAAM;AAErE,UAAI,OAAO,SAAS,OAAO,MAAM,QAAQ;AACvC,cAAM,QAAiC,CAAC;AACxC,mBAAW,SAAS,OAAO,MAAO,OAAM,KAAK,IAAI,KAAK,KAAK;AAC3D,YAAI,YAAY,MAAM,UAAU,EAAG,OAAM,WAAW,MAAM;AAC1D,YAAI,YAAY,MAAM,gBAAgB,EAAG,OAAM,iBAAiB,MAAM;AACtE,cAAM,WAAW,MAAM,IAAI,QAAQ,aAAoB,KAAY;AACnE,YAAI,UAAU;AACZ,qBAAW;AACX,kBAAQ,aAAa,EAAE,OAAO,GAAG,OAAO,QAAQ,OAAO,QAAQ,QAAQ,UAAU,CAAC;AAClF;AAAA,QACF;AAAA,MACF;AAEA,YAAM,SAAS,IAAI,OAAO,aAAoB,IAAW;AACzD,UAAI,QAAQ,MAAM;AAClB,YAAM,IAAI,MAAM;AAChB,iBAAW;AACX,cAAQ,aAAa,EAAE,OAAO,GAAG,OAAO,QAAQ,OAAO,QAAQ,QAAQ,UAAU,CAAC;AAAA,IACpF;AACA,QAAI,QAAQ,OAAQ,OAAM,IAAI,mBAAmB;AAAA,EACnD;AAEA,MAAI;AACF,UAAM,GAAG,cAAc,GAAG;AAAA,EAC5B,SAAS,KAAK;AACZ,QAAI,EAAE,eAAe,oBAAqB,OAAM;AAAA,EAClD;AAEA,SAAO,EAAE,OAAO,SAAS,QAAQ;AACnC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const SEED_DOCUMENT_FORMAT = "om-seed";
|
|
3
|
+
const SEED_DOCUMENT_VERSION = 1;
|
|
4
|
+
const ENCRYPTED_SEED_FORMAT = "om-encrypted-seed";
|
|
5
|
+
const ENCRYPTED_SEED_VERSION = 1;
|
|
6
|
+
const ENCRYPTED_SEED_ALGORITHM = "aes-256-gcm";
|
|
7
|
+
const seedRecordSchema = z.object({
|
|
8
|
+
entity: z.string().min(1),
|
|
9
|
+
match: z.array(z.string().min(1)).optional(),
|
|
10
|
+
data: z.record(z.string(), z.unknown())
|
|
11
|
+
});
|
|
12
|
+
const seedDocumentSchema = z.object({
|
|
13
|
+
format: z.literal(SEED_DOCUMENT_FORMAT),
|
|
14
|
+
version: z.literal(SEED_DOCUMENT_VERSION),
|
|
15
|
+
records: z.array(seedRecordSchema)
|
|
16
|
+
});
|
|
17
|
+
const encryptedSeedEnvelopeSchema = z.object({
|
|
18
|
+
format: z.literal(ENCRYPTED_SEED_FORMAT),
|
|
19
|
+
version: z.literal(ENCRYPTED_SEED_VERSION),
|
|
20
|
+
algorithm: z.literal(ENCRYPTED_SEED_ALGORITHM),
|
|
21
|
+
payload: z.string().min(1)
|
|
22
|
+
});
|
|
23
|
+
export {
|
|
24
|
+
ENCRYPTED_SEED_ALGORITHM,
|
|
25
|
+
ENCRYPTED_SEED_FORMAT,
|
|
26
|
+
ENCRYPTED_SEED_VERSION,
|
|
27
|
+
SEED_DOCUMENT_FORMAT,
|
|
28
|
+
SEED_DOCUMENT_VERSION,
|
|
29
|
+
encryptedSeedEnvelopeSchema,
|
|
30
|
+
seedDocumentSchema,
|
|
31
|
+
seedRecordSchema
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/seed/types.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\n\nexport const SEED_DOCUMENT_FORMAT = 'om-seed'\nexport const SEED_DOCUMENT_VERSION = 1\n\nexport const ENCRYPTED_SEED_FORMAT = 'om-encrypted-seed'\nexport const ENCRYPTED_SEED_VERSION = 1\nexport const ENCRYPTED_SEED_ALGORITHM = 'aes-256-gcm'\n\n/**\n * A single record to seed. `entity` is the platform entity id (`module:entity`,\n * e.g. `customers:customer_entity`). `data` keys are the entity's own property\n * names (camelCase, as declared on the MikroORM entity). `match`, when present,\n * lists property names used for an idempotent existence check before insert \u2014\n * these MUST be non-encrypted natural keys (id, slug, code, *_hash); encrypted\n * fields cannot be matched because their ciphertext is non-deterministic.\n */\nexport const seedRecordSchema = z.object({\n entity: z.string().min(1),\n match: z.array(z.string().min(1)).optional(),\n data: z.record(z.string(), z.unknown()),\n})\nexport type SeedRecord = z.infer<typeof seedRecordSchema>\n\n/**\n * The plaintext seed document. Records are applied in array order so authors can\n * satisfy foreign-key dependencies (create the parent before the child). The\n * document MUST NOT hard-code `tenantId`/`organizationId` \u2014 the loader injects\n * the target scope at load time so the same document seeds any tenant.\n */\nexport const seedDocumentSchema = z.object({\n format: z.literal(SEED_DOCUMENT_FORMAT),\n version: z.literal(SEED_DOCUMENT_VERSION),\n records: z.array(seedRecordSchema),\n})\nexport type SeedDocument = z.infer<typeof seedDocumentSchema>\n\n/**\n * The committed-to-repo, opaque envelope. `payload` is the encrypted seed\n * document in the shared AES-GCM `iv:ct:tag:v1` wire format.\n */\nexport const encryptedSeedEnvelopeSchema = z.object({\n format: z.literal(ENCRYPTED_SEED_FORMAT),\n version: z.literal(ENCRYPTED_SEED_VERSION),\n algorithm: z.literal(ENCRYPTED_SEED_ALGORITHM),\n payload: z.string().min(1),\n})\nexport type EncryptedSeedEnvelope = z.infer<typeof encryptedSeedEnvelopeSchema>\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAEX,MAAM,uBAAuB;AAC7B,MAAM,wBAAwB;AAE9B,MAAM,wBAAwB;AAC9B,MAAM,yBAAyB;AAC/B,MAAM,2BAA2B;AAUjC,MAAM,mBAAmB,EAAE,OAAO;AAAA,EACvC,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACxB,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,SAAS;AAAA,EAC3C,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AACxC,CAAC;AASM,MAAM,qBAAqB,EAAE,OAAO;AAAA,EACzC,QAAQ,EAAE,QAAQ,oBAAoB;AAAA,EACtC,SAAS,EAAE,QAAQ,qBAAqB;AAAA,EACxC,SAAS,EAAE,MAAM,gBAAgB;AACnC,CAAC;AAOM,MAAM,8BAA8B,EAAE,OAAO;AAAA,EAClD,QAAQ,EAAE,QAAQ,qBAAqB;AAAA,EACvC,SAAS,EAAE,QAAQ,sBAAsB;AAAA,EACzC,WAAW,EAAE,QAAQ,wBAAwB;AAAA,EAC7C,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC;AAC3B,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/version.ts"],
|
|
4
|
-
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.7.1-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.7.1-develop.7194.1.ab4fc81f82';\nexport const appVersion = APP_VERSION;\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -42,16 +42,32 @@ function getEventRegistryState() {
|
|
|
42
42
|
return fallbackEventRegistryState;
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
const CRUD_AFTER_EVENT_SUFFIXES = [".created", ".updated", ".deleted"];
|
|
46
|
+
const DEFAULT_CRUD_PAYLOAD_SCHEMA = {
|
|
47
|
+
fields: [
|
|
48
|
+
{ path: "id", type: "text" },
|
|
49
|
+
{ path: "organizationId", type: "text", optional: true },
|
|
50
|
+
{ path: "tenantId", type: "text", optional: true },
|
|
51
|
+
{ path: "syncOrigin", type: "text", optional: true }
|
|
52
|
+
]
|
|
53
|
+
};
|
|
54
|
+
function applyDefaultCrudPayloadSchema(event) {
|
|
55
|
+
if (event.payloadSchema) return event;
|
|
56
|
+
if (event.category !== "crud") return event;
|
|
57
|
+
if (!CRUD_AFTER_EVENT_SUFFIXES.some((suffix) => event.id.endsWith(suffix))) return event;
|
|
58
|
+
return { ...event, payloadSchema: DEFAULT_CRUD_PAYLOAD_SCHEMA };
|
|
59
|
+
}
|
|
45
60
|
function addDeclaredEvent(event) {
|
|
61
|
+
const declared = applyDefaultCrudPayloadSchema(event);
|
|
46
62
|
const state = getEventRegistryState();
|
|
47
|
-
state.declaredEventIds.add(
|
|
48
|
-
const existingIndex = state.declaredEvents.findIndex((candidate) => candidate.id ===
|
|
63
|
+
state.declaredEventIds.add(declared.id);
|
|
64
|
+
const existingIndex = state.declaredEvents.findIndex((candidate) => candidate.id === declared.id);
|
|
49
65
|
if (existingIndex < 0) {
|
|
50
|
-
state.declaredEvents.push(
|
|
66
|
+
state.declaredEvents.push(declared);
|
|
51
67
|
return;
|
|
52
68
|
}
|
|
53
|
-
if (state.declaredEvents[existingIndex]?.module ===
|
|
54
|
-
state.declaredEvents[existingIndex] =
|
|
69
|
+
if (state.declaredEvents[existingIndex]?.module === declared.module) {
|
|
70
|
+
state.declaredEvents[existingIndex] = declared;
|
|
55
71
|
}
|
|
56
72
|
}
|
|
57
73
|
function isEventDeclared(eventId) {
|
|
@@ -102,10 +118,12 @@ function getEventModuleConfigs() {
|
|
|
102
118
|
function createModuleEvents(options) {
|
|
103
119
|
const { moduleId, events, strict = false } = options;
|
|
104
120
|
const validEventIds = new Set(events.map((e) => e.id));
|
|
105
|
-
const fullEvents = events.map(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
121
|
+
const fullEvents = events.map(
|
|
122
|
+
(e) => applyDefaultCrudPayloadSchema({
|
|
123
|
+
...e,
|
|
124
|
+
module: moduleId
|
|
125
|
+
})
|
|
126
|
+
);
|
|
109
127
|
for (const event of fullEvents) {
|
|
110
128
|
addDeclaredEvent(event);
|
|
111
129
|
}
|
|
@@ -144,6 +162,7 @@ function createModuleEvents(options) {
|
|
|
144
162
|
};
|
|
145
163
|
}
|
|
146
164
|
export {
|
|
165
|
+
DEFAULT_CRUD_PAYLOAD_SCHEMA,
|
|
147
166
|
createModuleEvents,
|
|
148
167
|
getAllDeclaredEventIds,
|
|
149
168
|
getDeclaredEvents,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/events/factory.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Event Module Factory\n *\n * Provides factory functions for creating type-safe event configurations.\n */\n\nimport { createLogger } from '../../lib/logger'\nimport type {\n EventDefinition,\n EventModuleConfig,\n EventPayload,\n EmitOptions,\n CreateModuleEventsOptions,\n ModuleEventEmitter,\n} from './types'\n\nconst logger = createLogger('events').child({ component: 'factory' })\n\n// =============================================================================\n// Global Event Bus Reference\n// =============================================================================\n\n/**\n * Type for the global event bus interface\n */\ninterface GlobalEventBus {\n emit(event: string, payload: unknown, options?: EmitOptions): Promise<void>\n}\n\nconst GLOBAL_EVENT_BUS_KEY = '__openMercatoGlobalEventBus__'\n\n// Global event bus reference (set during bootstrap)\nlet globalEventBus: GlobalEventBus | null = null\n\n/**\n * Set the global event bus instance.\n * Called during app bootstrap to wire up event emission.\n */\nexport function setGlobalEventBus(bus: GlobalEventBus): void {\n globalEventBus = bus\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY] = bus\n } catch {\n // ignore global assignment failures\n }\n}\n\n/**\n * Get the global event bus instance.\n * Returns null if not yet bootstrapped.\n */\nexport function getGlobalEventBus(): GlobalEventBus | null {\n try {\n const sharedBus = (globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY]\n if (sharedBus && typeof sharedBus === 'object' && typeof (sharedBus as GlobalEventBus).emit === 'function') {\n return sharedBus as GlobalEventBus\n }\n } catch {\n // ignore global read failures\n }\n return globalEventBus\n}\n\n// =============================================================================\n// Event Registry for Validation\n// =============================================================================\n\ntype EventRegistryState = {\n declaredEventIds: Set<string>\n declaredEvents: EventDefinition[]\n registeredEventConfigs: EventModuleConfig[] | null\n}\n\nconst GLOBAL_EVENT_REGISTRY_KEY = '__openMercatoEventDefinitionRegistry__'\n\nconst fallbackEventRegistryState: EventRegistryState = {\n declaredEventIds: new Set<string>(),\n declaredEvents: [],\n registeredEventConfigs: null,\n}\n\nfunction isEventRegistryState(value: unknown): value is EventRegistryState {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Partial<EventRegistryState>\n return candidate.declaredEventIds instanceof Set\n && Array.isArray(candidate.declaredEvents)\n && (candidate.registeredEventConfigs === null || Array.isArray(candidate.registeredEventConfigs))\n}\n\nfunction getEventRegistryState(): EventRegistryState {\n try {\n const globalScope = globalThis as Record<string, unknown>\n const existing = globalScope[GLOBAL_EVENT_REGISTRY_KEY]\n if (isEventRegistryState(existing)) return existing\n globalScope[GLOBAL_EVENT_REGISTRY_KEY] = fallbackEventRegistryState\n return fallbackEventRegistryState\n } catch {\n // Restricted runtimes may deny global access. Keep the previous\n // module-local behavior as a safe fallback.\n return fallbackEventRegistryState\n }\n}\n\nfunction addDeclaredEvent(event: EventDefinition): void {\n const state = getEventRegistryState()\n state.declaredEventIds.add(event.id)\n const existingIndex = state.declaredEvents.findIndex((candidate) => candidate.id === event.id)\n if (existingIndex < 0) {\n state.declaredEvents.push(event)\n return\n }\n // Refresh a module's own definition in place during HMR without allowing a\n // duplicate declaration from another module to take over the event id.\n if (state.declaredEvents[existingIndex]?.module === event.module) {\n state.declaredEvents[existingIndex] = event\n }\n}\n\n/**\n * Check if an event ID has been declared by any module.\n * Used for runtime validation to ensure only declared events are emitted.\n */\nexport function isEventDeclared(eventId: string): boolean {\n return getEventRegistryState().declaredEventIds.has(eventId)\n}\n\n/**\n * Get all declared event IDs.\n * Useful for debugging and introspection.\n */\nexport function getAllDeclaredEventIds(): string[] {\n return Array.from(getEventRegistryState().declaredEventIds)\n}\n\n/**\n * Get all declared events with their full definitions.\n * Used by the API to return available events for workflow triggers.\n */\nexport function getDeclaredEvents(): EventDefinition[] {\n return [...getEventRegistryState().declaredEvents]\n}\n\n/**\n * Check if an event has clientBroadcast enabled.\n * Used by the SSE endpoint to filter events for the DOM Event Bridge.\n */\nexport function isBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.clientBroadcast === true\n}\n\n/**\n * Check if an event should be published over the server-to-server event bridge.\n * Browser-broadcast events remain eligible for backward compatibility, while\n * crossProcessBroadcast supports private process coordination without SSE.\n */\nexport function isCrossProcessBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.clientBroadcast === true || event?.crossProcessBroadcast === true\n}\n\n/**\n * Check whether an event is reserved for private server-to-server\n * coordination. Workflow-authored EMIT_EVENT activities must not emit these\n * events because their payload and event id are tenant-managed input.\n */\nexport function isPrivateCrossProcessBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.crossProcessBroadcast === true && event?.clientBroadcast !== true\n}\n\n/**\n * Verify provenance for a private cross-process event. The module id is\n * stamped by a declared module emitter or another trusted server-side seam;\n * tenant-managed event payloads never participate in this decision.\n */\nexport function isPrivateCrossProcessEventEmitter(\n eventId: string,\n emitterModuleId: string | undefined,\n): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n if (event?.crossProcessBroadcast !== true) return true\n return typeof event.module === 'string'\n && event.module.length > 0\n && event.module === emitterModuleId\n}\n\n/**\n * Check if an event has portalBroadcast enabled.\n * Used by the portal SSE endpoint to filter events for the Portal Event Bridge.\n */\nexport function isPortalBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.portalBroadcast === true\n}\n\n// =============================================================================\n// Bootstrap Registration (similar to searchModuleConfigs pattern)\n// =============================================================================\n\n/**\n * Register event module configurations globally.\n * Called during app bootstrap with configs from events.generated.ts.\n */\nexport function registerEventModuleConfigs(configs: EventModuleConfig[]): void {\n const state = getEventRegistryState()\n if (state.registeredEventConfigs !== null && process.env.NODE_ENV === 'development') {\n logger.debug('Event module configs re-registered (this may occur during HMR)')\n }\n state.registeredEventConfigs = configs\n for (const config of configs) {\n for (const event of config.events) {\n addDeclaredEvent(event)\n }\n }\n}\n\n/**\n * Get registered event module configurations.\n * Returns empty array if not registered.\n */\nexport function getEventModuleConfigs(): EventModuleConfig[] {\n return getEventRegistryState().registeredEventConfigs ?? []\n}\n\n// =============================================================================\n// Factory Function\n// =============================================================================\n\n/**\n * Creates a type-safe event configuration for a module.\n *\n * Usage in module events.ts:\n * ```typescript\n * import { createModuleEvents } from '@open-mercato/shared/modules/events'\n *\n * const events = [\n * { id: 'customers.people.created', label: 'Person Created', category: 'crud' },\n * { id: 'customers.people.updated', label: 'Person Updated', category: 'crud' },\n * ] as const\n *\n * export const eventsConfig = createModuleEvents({\n * moduleId: 'customers',\n * events,\n * })\n *\n * // Export the typed emit function for use in commands\n * export const emitCustomersEvent = eventsConfig.emit\n *\n * // Export event IDs as a type for external use\n * export type CustomersEventId = typeof events[number]['id']\n *\n * export default eventsConfig\n * ```\n *\n * TypeScript will enforce that only declared event IDs can be emitted:\n * ```typescript\n * // \u2705 This compiles - event is declared\n * emitCustomersEvent('customers.people.created', { id: '123', tenantId: 'abc' })\n *\n * // \u274C TypeScript error - event not declared\n * emitCustomersEvent('customers.people.exploded', { id: '123' })\n * ```\n */\nexport function createModuleEvents<\n const TEvents extends readonly { id: string }[],\n TEventIds extends TEvents[number]['id'] = TEvents[number]['id']\n>(options: CreateModuleEventsOptions<TEventIds>): EventModuleConfig<TEventIds> {\n const { moduleId, events, strict = false } = options\n\n // Build set of valid event IDs for runtime validation\n const validEventIds = new Set(events.map(e => e.id))\n\n // Build full event definitions with module added\n const fullEvents: EventDefinition[] = events.map(e => ({\n ...e,\n module: moduleId,\n }))\n\n // Register all event IDs and definitions in the global registry.\n for (const event of fullEvents) {\n addDeclaredEvent(event)\n }\n\n /**\n * The emit function - validates events and delegates to the global event bus\n */\n const emit = async (\n eventId: TEventIds,\n payload: EventPayload,\n emitOptions?: EmitOptions\n ): Promise<void> => {\n // Runtime validation - event must be declared\n if (!validEventIds.has(eventId)) {\n const message =\n `[events] Module \"${moduleId}\" tried to emit undeclared event \"${eventId}\". ` +\n `Add it to the module's events.ts file first.`\n\n if (strict) {\n throw new Error(message)\n } else {\n logger.error('Module tried to emit undeclared event \u2014 add it to the module events.ts first', { moduleId, eventId })\n // In non-strict mode, still emit but with warning\n }\n }\n\n // Get event bus from global reference\n const eventBus = getGlobalEventBus()\n if (!eventBus) {\n logger.warn('Event bus not available, cannot emit event', { eventId })\n return\n }\n\n const eventDefinition = fullEvents.find((event) => event.id === eventId)\n const isClientBroadcast = eventDefinition?.clientBroadcast === true\n const trustedOptions = eventDefinition?.crossProcessBroadcast === true || isClientBroadcast\n ? {\n ...emitOptions,\n // Browser-broadcast module emitters historically accepted scope in\n // their typed payload. Preserve that contract at the trusted module\n // boundary while the event bus itself relies only on options.\n ...(isClientBroadcast && emitOptions?.tenantId === undefined\n ? { tenantId: payload.tenantId ?? null }\n : {}),\n ...(isClientBroadcast && emitOptions?.organizationId === undefined\n ? { organizationId: payload.organizationId ?? null }\n : {}),\n ...(isClientBroadcast && emitOptions?.organizationIds === undefined && Array.isArray(payload.organizationIds)\n ? { organizationIds: payload.organizationIds.filter((value): value is string => typeof value === 'string') }\n : {}),\n emitterModuleId: moduleId,\n }\n : emitOptions\n await eventBus.emit(eventId, payload, trustedOptions)\n }\n\n return {\n moduleId,\n events: fullEvents,\n emit: emit as unknown as ModuleEventEmitter<TEventIds>,\n }\n}\n"],
|
|
5
|
-
"mappings": "AAMA,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["/**\n * Event Module Factory\n *\n * Provides factory functions for creating type-safe event configurations.\n */\n\nimport { createLogger } from '../../lib/logger'\nimport type {\n EventDefinition,\n EventModuleConfig,\n EventPayload,\n EventPayloadSchema,\n EmitOptions,\n CreateModuleEventsOptions,\n ModuleEventEmitter,\n} from './types'\n\nconst logger = createLogger('events').child({ component: 'factory' })\n\n// =============================================================================\n// Global Event Bus Reference\n// =============================================================================\n\n/**\n * Type for the global event bus interface\n */\ninterface GlobalEventBus {\n emit(event: string, payload: unknown, options?: EmitOptions): Promise<void>\n}\n\nconst GLOBAL_EVENT_BUS_KEY = '__openMercatoGlobalEventBus__'\n\n// Global event bus reference (set during bootstrap)\nlet globalEventBus: GlobalEventBus | null = null\n\n/**\n * Set the global event bus instance.\n * Called during app bootstrap to wire up event emission.\n */\nexport function setGlobalEventBus(bus: GlobalEventBus): void {\n globalEventBus = bus\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY] = bus\n } catch {\n // ignore global assignment failures\n }\n}\n\n/**\n * Get the global event bus instance.\n * Returns null if not yet bootstrapped.\n */\nexport function getGlobalEventBus(): GlobalEventBus | null {\n try {\n const sharedBus = (globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY]\n if (sharedBus && typeof sharedBus === 'object' && typeof (sharedBus as GlobalEventBus).emit === 'function') {\n return sharedBus as GlobalEventBus\n }\n } catch {\n // ignore global read failures\n }\n return globalEventBus\n}\n\n// =============================================================================\n// Event Registry for Validation\n// =============================================================================\n\ntype EventRegistryState = {\n declaredEventIds: Set<string>\n declaredEvents: EventDefinition[]\n registeredEventConfigs: EventModuleConfig[] | null\n}\n\nconst GLOBAL_EVENT_REGISTRY_KEY = '__openMercatoEventDefinitionRegistry__'\n\nconst fallbackEventRegistryState: EventRegistryState = {\n declaredEventIds: new Set<string>(),\n declaredEvents: [],\n registeredEventConfigs: null,\n}\n\nfunction isEventRegistryState(value: unknown): value is EventRegistryState {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Partial<EventRegistryState>\n return candidate.declaredEventIds instanceof Set\n && Array.isArray(candidate.declaredEvents)\n && (candidate.registeredEventConfigs === null || Array.isArray(candidate.registeredEventConfigs))\n}\n\nfunction getEventRegistryState(): EventRegistryState {\n try {\n const globalScope = globalThis as Record<string, unknown>\n const existing = globalScope[GLOBAL_EVENT_REGISTRY_KEY]\n if (isEventRegistryState(existing)) return existing\n globalScope[GLOBAL_EVENT_REGISTRY_KEY] = fallbackEventRegistryState\n return fallbackEventRegistryState\n } catch {\n // Restricted runtimes may deny global access. Keep the previous\n // module-local behavior as a safe fallback.\n return fallbackEventRegistryState\n }\n}\n\nconst CRUD_AFTER_EVENT_SUFFIXES = ['.created', '.updated', '.deleted'] as const\n\n/**\n * Generated payload schema for platform-emitted CRUD after-events. Mirrors the\n * default payload built by the data engine's `emitOrmEntityEvent` when no\n * `buildPayload` override is configured: `{ id, organizationId, tenantId }`\n * plus `syncOrigin` when the write originated from a sync. organizationId and\n * tenantId keys are always present but may be null, so they are `optional`.\n */\nexport const DEFAULT_CRUD_PAYLOAD_SCHEMA: EventPayloadSchema = {\n fields: [\n { path: 'id', type: 'text' },\n { path: 'organizationId', type: 'text', optional: true },\n { path: 'tenantId', type: 'text', optional: true },\n { path: 'syncOrigin', type: 'text', optional: true },\n ],\n}\n\nfunction applyDefaultCrudPayloadSchema(event: EventDefinition): EventDefinition {\n if (event.payloadSchema) return event\n if (event.category !== 'crud') return event\n if (!CRUD_AFTER_EVENT_SUFFIXES.some(suffix => event.id.endsWith(suffix))) return event\n return { ...event, payloadSchema: DEFAULT_CRUD_PAYLOAD_SCHEMA }\n}\n\nfunction addDeclaredEvent(event: EventDefinition): void {\n const declared = applyDefaultCrudPayloadSchema(event)\n const state = getEventRegistryState()\n state.declaredEventIds.add(declared.id)\n const existingIndex = state.declaredEvents.findIndex((candidate) => candidate.id === declared.id)\n if (existingIndex < 0) {\n state.declaredEvents.push(declared)\n return\n }\n // Refresh a module's own definition in place during HMR without allowing a\n // duplicate declaration from another module to take over the event id.\n if (state.declaredEvents[existingIndex]?.module === declared.module) {\n state.declaredEvents[existingIndex] = declared\n }\n}\n\n/**\n * Check if an event ID has been declared by any module.\n * Used for runtime validation to ensure only declared events are emitted.\n */\nexport function isEventDeclared(eventId: string): boolean {\n return getEventRegistryState().declaredEventIds.has(eventId)\n}\n\n/**\n * Get all declared event IDs.\n * Useful for debugging and introspection.\n */\nexport function getAllDeclaredEventIds(): string[] {\n return Array.from(getEventRegistryState().declaredEventIds)\n}\n\n/**\n * Get all declared events with their full definitions.\n * Used by the API to return available events for workflow triggers.\n */\nexport function getDeclaredEvents(): EventDefinition[] {\n return [...getEventRegistryState().declaredEvents]\n}\n\n/**\n * Check if an event has clientBroadcast enabled.\n * Used by the SSE endpoint to filter events for the DOM Event Bridge.\n */\nexport function isBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.clientBroadcast === true\n}\n\n/**\n * Check if an event should be published over the server-to-server event bridge.\n * Browser-broadcast events remain eligible for backward compatibility, while\n * crossProcessBroadcast supports private process coordination without SSE.\n */\nexport function isCrossProcessBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.clientBroadcast === true || event?.crossProcessBroadcast === true\n}\n\n/**\n * Check whether an event is reserved for private server-to-server\n * coordination. Workflow-authored EMIT_EVENT activities must not emit these\n * events because their payload and event id are tenant-managed input.\n */\nexport function isPrivateCrossProcessBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.crossProcessBroadcast === true && event?.clientBroadcast !== true\n}\n\n/**\n * Verify provenance for a private cross-process event. The module id is\n * stamped by a declared module emitter or another trusted server-side seam;\n * tenant-managed event payloads never participate in this decision.\n */\nexport function isPrivateCrossProcessEventEmitter(\n eventId: string,\n emitterModuleId: string | undefined,\n): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n if (event?.crossProcessBroadcast !== true) return true\n return typeof event.module === 'string'\n && event.module.length > 0\n && event.module === emitterModuleId\n}\n\n/**\n * Check if an event has portalBroadcast enabled.\n * Used by the portal SSE endpoint to filter events for the Portal Event Bridge.\n */\nexport function isPortalBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.portalBroadcast === true\n}\n\n// =============================================================================\n// Bootstrap Registration (similar to searchModuleConfigs pattern)\n// =============================================================================\n\n/**\n * Register event module configurations globally.\n * Called during app bootstrap with configs from events.generated.ts.\n */\nexport function registerEventModuleConfigs(configs: EventModuleConfig[]): void {\n const state = getEventRegistryState()\n if (state.registeredEventConfigs !== null && process.env.NODE_ENV === 'development') {\n logger.debug('Event module configs re-registered (this may occur during HMR)')\n }\n state.registeredEventConfigs = configs\n for (const config of configs) {\n for (const event of config.events) {\n addDeclaredEvent(event)\n }\n }\n}\n\n/**\n * Get registered event module configurations.\n * Returns empty array if not registered.\n */\nexport function getEventModuleConfigs(): EventModuleConfig[] {\n return getEventRegistryState().registeredEventConfigs ?? []\n}\n\n// =============================================================================\n// Factory Function\n// =============================================================================\n\n/**\n * Creates a type-safe event configuration for a module.\n *\n * Usage in module events.ts:\n * ```typescript\n * import { createModuleEvents } from '@open-mercato/shared/modules/events'\n *\n * const events = [\n * { id: 'customers.people.created', label: 'Person Created', category: 'crud' },\n * { id: 'customers.people.updated', label: 'Person Updated', category: 'crud' },\n * ] as const\n *\n * export const eventsConfig = createModuleEvents({\n * moduleId: 'customers',\n * events,\n * })\n *\n * // Export the typed emit function for use in commands\n * export const emitCustomersEvent = eventsConfig.emit\n *\n * // Export event IDs as a type for external use\n * export type CustomersEventId = typeof events[number]['id']\n *\n * export default eventsConfig\n * ```\n *\n * TypeScript will enforce that only declared event IDs can be emitted:\n * ```typescript\n * // \u2705 This compiles - event is declared\n * emitCustomersEvent('customers.people.created', { id: '123', tenantId: 'abc' })\n *\n * // \u274C TypeScript error - event not declared\n * emitCustomersEvent('customers.people.exploded', { id: '123' })\n * ```\n */\nexport function createModuleEvents<\n const TEvents extends readonly { id: string }[],\n TEventIds extends TEvents[number]['id'] = TEvents[number]['id']\n>(options: CreateModuleEventsOptions<TEventIds>): EventModuleConfig<TEventIds> {\n const { moduleId, events, strict = false } = options\n\n // Build set of valid event IDs for runtime validation\n const validEventIds = new Set(events.map(e => e.id))\n\n // Build full event definitions with module added and the generated CRUD\n // payload-schema default applied, so config consumers and the global\n // registry see the same definitions.\n const fullEvents: EventDefinition[] = events.map(e =>\n applyDefaultCrudPayloadSchema({\n ...e,\n module: moduleId,\n }),\n )\n\n // Register all event IDs and definitions in the global registry.\n for (const event of fullEvents) {\n addDeclaredEvent(event)\n }\n\n /**\n * The emit function - validates events and delegates to the global event bus\n */\n const emit = async (\n eventId: TEventIds,\n payload: EventPayload,\n emitOptions?: EmitOptions\n ): Promise<void> => {\n // Runtime validation - event must be declared\n if (!validEventIds.has(eventId)) {\n const message =\n `[events] Module \"${moduleId}\" tried to emit undeclared event \"${eventId}\". ` +\n `Add it to the module's events.ts file first.`\n\n if (strict) {\n throw new Error(message)\n } else {\n logger.error('Module tried to emit undeclared event \u2014 add it to the module events.ts first', { moduleId, eventId })\n // In non-strict mode, still emit but with warning\n }\n }\n\n // Get event bus from global reference\n const eventBus = getGlobalEventBus()\n if (!eventBus) {\n logger.warn('Event bus not available, cannot emit event', { eventId })\n return\n }\n\n const eventDefinition = fullEvents.find((event) => event.id === eventId)\n const isClientBroadcast = eventDefinition?.clientBroadcast === true\n const trustedOptions = eventDefinition?.crossProcessBroadcast === true || isClientBroadcast\n ? {\n ...emitOptions,\n // Browser-broadcast module emitters historically accepted scope in\n // their typed payload. Preserve that contract at the trusted module\n // boundary while the event bus itself relies only on options.\n ...(isClientBroadcast && emitOptions?.tenantId === undefined\n ? { tenantId: payload.tenantId ?? null }\n : {}),\n ...(isClientBroadcast && emitOptions?.organizationId === undefined\n ? { organizationId: payload.organizationId ?? null }\n : {}),\n ...(isClientBroadcast && emitOptions?.organizationIds === undefined && Array.isArray(payload.organizationIds)\n ? { organizationIds: payload.organizationIds.filter((value): value is string => typeof value === 'string') }\n : {}),\n emitterModuleId: moduleId,\n }\n : emitOptions\n await eventBus.emit(eventId, payload, trustedOptions)\n }\n\n return {\n moduleId,\n events: fullEvents,\n emit: emit as unknown as ModuleEventEmitter<TEventIds>,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/shared",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7194.1.ab4fc81f82",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -113,10 +113,11 @@
|
|
|
113
113
|
"@mikro-orm/core": "^7.1.14",
|
|
114
114
|
"@mikro-orm/decorators": "^7.1.14",
|
|
115
115
|
"@mikro-orm/postgresql": "^7.1.14",
|
|
116
|
-
"@open-mercato/cache": "0.7.1-develop.
|
|
116
|
+
"@open-mercato/cache": "0.7.1-develop.7194.1.ab4fc81f82",
|
|
117
117
|
"@types/html-to-text": "^9.0.4",
|
|
118
118
|
"@types/sanitize-html": "^2.16.1",
|
|
119
119
|
"dotenv": "^17.4.2",
|
|
120
|
+
"gpt-tokenizer": "^3.4.0",
|
|
120
121
|
"html-to-text": "^10.0.0",
|
|
121
122
|
"pino": "^10.3.1",
|
|
122
123
|
"rate-limiter-flexible": "^11.2.0",
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { normalizeOpenCodeToolPart } from '../opencode-tool-parts'
|
|
2
|
+
|
|
3
|
+
describe('normalizeOpenCodeToolPart', () => {
|
|
4
|
+
it('opens a native tool part on a non-terminal state', () => {
|
|
5
|
+
const update = normalizeOpenCodeToolPart({
|
|
6
|
+
type: 'tool',
|
|
7
|
+
id: 'prt-1',
|
|
8
|
+
callID: 'call-1',
|
|
9
|
+
tool: 'load_skill',
|
|
10
|
+
state: { status: 'running', input: { skillId: 'x' } },
|
|
11
|
+
})
|
|
12
|
+
expect(update).toEqual({ phase: 'progress', callId: 'call-1', toolName: 'load_skill', input: { skillId: 'x' } })
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('finishes a native tool part on completed, carrying the output', () => {
|
|
16
|
+
const update = normalizeOpenCodeToolPart({
|
|
17
|
+
type: 'tool',
|
|
18
|
+
callID: 'call-1',
|
|
19
|
+
tool: 'load_skill',
|
|
20
|
+
state: { status: 'completed', input: { skillId: 'x' }, output: { ok: true } },
|
|
21
|
+
})
|
|
22
|
+
expect(update).toEqual({
|
|
23
|
+
phase: 'finish',
|
|
24
|
+
callId: 'call-1',
|
|
25
|
+
toolName: 'load_skill',
|
|
26
|
+
input: { skillId: 'x' },
|
|
27
|
+
output: { ok: true },
|
|
28
|
+
status: 'ok',
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('maps an errored native tool part to status error, preferring state.error', () => {
|
|
33
|
+
const update = normalizeOpenCodeToolPart({
|
|
34
|
+
type: 'tool',
|
|
35
|
+
callID: 'call-2',
|
|
36
|
+
tool: 'run_skill_script',
|
|
37
|
+
state: { status: 'error', error: 'boom' },
|
|
38
|
+
})
|
|
39
|
+
expect(update).toEqual({
|
|
40
|
+
phase: 'finish',
|
|
41
|
+
callId: 'call-2',
|
|
42
|
+
toolName: 'run_skill_script',
|
|
43
|
+
input: undefined,
|
|
44
|
+
output: 'boom',
|
|
45
|
+
status: 'error',
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('falls back to part.id when callID is absent', () => {
|
|
50
|
+
const update = normalizeOpenCodeToolPart({
|
|
51
|
+
type: 'tool',
|
|
52
|
+
id: 'prt-9',
|
|
53
|
+
tool: 'search',
|
|
54
|
+
state: { status: 'running' },
|
|
55
|
+
})
|
|
56
|
+
expect(update).toMatchObject({ phase: 'progress', callId: 'prt-9', toolName: 'search' })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('handles the legacy tool_use / tool_result shape', () => {
|
|
60
|
+
expect(normalizeOpenCodeToolPart({ type: 'tool_use', id: 'tc-1', name: 'load_skill', input: { a: 1 } })).toEqual({
|
|
61
|
+
phase: 'progress',
|
|
62
|
+
callId: 'tc-1',
|
|
63
|
+
toolName: 'load_skill',
|
|
64
|
+
input: { a: 1 },
|
|
65
|
+
})
|
|
66
|
+
expect(normalizeOpenCodeToolPart({ type: 'tool_result', tool_use_id: 'tc-1', content: { ok: true } })).toEqual({
|
|
67
|
+
phase: 'finish',
|
|
68
|
+
callId: 'tc-1',
|
|
69
|
+
output: { ok: true },
|
|
70
|
+
status: 'ok',
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('ignores non-tool and malformed parts', () => {
|
|
75
|
+
expect(normalizeOpenCodeToolPart({ type: 'text', text: 'hi' })).toBeNull()
|
|
76
|
+
expect(normalizeOpenCodeToolPart({ type: 'thinking' })).toBeNull()
|
|
77
|
+
expect(normalizeOpenCodeToolPart({ type: 'tool' })).toBeNull() // no callID/tool
|
|
78
|
+
expect(normalizeOpenCodeToolPart(null)).toBeNull()
|
|
79
|
+
expect(normalizeOpenCodeToolPart('nope')).toBeNull()
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { countTokens, TOKEN_ENCODING } from '../token-count'
|
|
2
|
+
|
|
3
|
+
describe('countTokens', () => {
|
|
4
|
+
it('returns 0 for empty / nullish input', () => {
|
|
5
|
+
expect(countTokens('')).toBe(0)
|
|
6
|
+
expect(countTokens(null)).toBe(0)
|
|
7
|
+
expect(countTokens(undefined)).toBe(0)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('counts more tokens for longer text', () => {
|
|
11
|
+
const short = countTokens('hello')
|
|
12
|
+
const long = countTokens('hello world, this is a noticeably longer sentence.')
|
|
13
|
+
expect(short).toBeGreaterThan(0)
|
|
14
|
+
expect(long).toBeGreaterThan(short)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('exposes the encoding label', () => {
|
|
18
|
+
expect(TOKEN_ENCODING).toBe('o200k_base')
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes an OpenCode `message.part.updated` part into a tool-call lifecycle
|
|
3
|
+
* update, shielding callers from OpenCode's wire schema.
|
|
4
|
+
*
|
|
5
|
+
* OpenCode (Go server) streams MCP tool invocations as parts of `type: 'tool'`
|
|
6
|
+
* carrying a `callID`, the `tool` name, and a `state` machine
|
|
7
|
+
* (`state.status: pending|running|completed|error`, `state.input`,
|
|
8
|
+
* `state.output`/`state.error`). The same part id is re-emitted on each state
|
|
9
|
+
* transition, so a tool call surfaces as one or more `progress` updates followed
|
|
10
|
+
* by a single `finish` once the state reaches a terminal status.
|
|
11
|
+
*
|
|
12
|
+
* Older OpenCode builds emitted Anthropic-style `tool_use` / `tool_result`
|
|
13
|
+
* blocks instead; those are still recognized as a fallback so a downgrade does
|
|
14
|
+
* not silently drop traces again.
|
|
15
|
+
*
|
|
16
|
+
* Returns `null` for any part that is not a tool invocation (text, thinking,
|
|
17
|
+
* step markers, …) so callers can ignore it.
|
|
18
|
+
*/
|
|
19
|
+
export type OpenCodeToolPartUpdate =
|
|
20
|
+
| { phase: 'progress'; callId: string; toolName: string; input?: unknown }
|
|
21
|
+
| {
|
|
22
|
+
phase: 'finish'
|
|
23
|
+
callId: string
|
|
24
|
+
toolName?: string
|
|
25
|
+
input?: unknown
|
|
26
|
+
output?: unknown
|
|
27
|
+
status: 'ok' | 'error'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function asString(value: unknown): string | undefined {
|
|
31
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
35
|
+
return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function normalizeOpenCodeToolPart(rawPart: unknown): OpenCodeToolPartUpdate | null {
|
|
39
|
+
if (!rawPart || typeof rawPart !== 'object') return null
|
|
40
|
+
const part = rawPart as Record<string, unknown>
|
|
41
|
+
const type = asString(part.type)
|
|
42
|
+
if (!type) return null
|
|
43
|
+
|
|
44
|
+
// Native OpenCode tool part with a state machine.
|
|
45
|
+
if (type === 'tool') {
|
|
46
|
+
const callId = asString(part.callID) ?? asString(part.id)
|
|
47
|
+
const toolName = asString(part.tool)
|
|
48
|
+
if (!callId || !toolName) return null
|
|
49
|
+
const state = asRecord(part.state)
|
|
50
|
+
const status = asString(state.status)
|
|
51
|
+
const input = 'input' in state ? state.input : undefined
|
|
52
|
+
if (status === 'completed' || status === 'error') {
|
|
53
|
+
const output = status === 'error' ? state.error ?? state.output : state.output
|
|
54
|
+
return {
|
|
55
|
+
phase: 'finish',
|
|
56
|
+
callId,
|
|
57
|
+
toolName,
|
|
58
|
+
input,
|
|
59
|
+
output,
|
|
60
|
+
status: status === 'error' ? 'error' : 'ok',
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { phase: 'progress', callId, toolName, input }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Legacy Anthropic-style parts (older OpenCode builds).
|
|
67
|
+
if (type === 'tool_use') {
|
|
68
|
+
const callId = asString(part.id)
|
|
69
|
+
const toolName = asString(part.name)
|
|
70
|
+
if (!callId || !toolName) return null
|
|
71
|
+
return { phase: 'progress', callId, toolName, input: part.input }
|
|
72
|
+
}
|
|
73
|
+
if (type === 'tool_result') {
|
|
74
|
+
const callId = asString(part.tool_use_id) ?? asString(part.id)
|
|
75
|
+
if (!callId) return null
|
|
76
|
+
return { phase: 'finish', callId, output: part.content, status: 'ok' }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { encode } from 'gpt-tokenizer/encoding/o200k_base'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Model-agnostic offline token estimate.
|
|
5
|
+
*
|
|
6
|
+
* Uses the `o200k_base` BPE encoding (GPT-4o / GPT-5 family) as a proxy. It is
|
|
7
|
+
* NOT exact for non-OpenAI models — notably Claude, whose tokenizer is not
|
|
8
|
+
* available offline — but it is deterministic, dependency-light, and a far
|
|
9
|
+
* closer estimate than a chars/4 heuristic. Treat the result as an estimate.
|
|
10
|
+
*
|
|
11
|
+
* Infrastructure only: this file knows nothing about any domain shape. Callers
|
|
12
|
+
* that need to break a structure down into elements assemble their own totals
|
|
13
|
+
* on top of this primitive.
|
|
14
|
+
*/
|
|
15
|
+
export function countTokens(text: string | null | undefined): number {
|
|
16
|
+
if (!text) return 0
|
|
17
|
+
return encode(text).length
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The BPE encoding backing {@link countTokens}, surfaced so callers can label estimates. */
|
|
21
|
+
export const TOKEN_ENCODING = 'o200k_base' as const
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { asValue } from 'awilix'
|
|
1
2
|
import type { BootstrapData } from './types'
|
|
2
3
|
import type { AppDiRegistrar } from '../di/container'
|
|
3
4
|
import { findAppRoot, type AppRoot } from './appResolver'
|
|
@@ -497,6 +498,21 @@ async function compileAndImport(
|
|
|
497
498
|
}
|
|
498
499
|
|
|
499
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Registers an app-owned generated value on the request container.
|
|
503
|
+
*
|
|
504
|
+
* The app registers these statically from `src/di.ts`, which `createRequestContainer`
|
|
505
|
+
* reaches through the `@/` alias — and that alias only exists under the bundler.
|
|
506
|
+
* A CLI or MCP process runs plain Node, so the import fails, the failure is
|
|
507
|
+
* swallowed, and the value is simply absent with no diagnostic. Routing it through
|
|
508
|
+
* a registrar built from the same generated file keeps both processes in step.
|
|
509
|
+
*/
|
|
510
|
+
function appValueRegistrar(key: string, value: unknown): BootstrapData['diRegistrars'][number] {
|
|
511
|
+
return (container) => {
|
|
512
|
+
container.register({ [key]: asValue(value) })
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
500
516
|
/**
|
|
501
517
|
* Load a generated registry that older apps may not have generated yet.
|
|
502
518
|
*
|
|
@@ -716,6 +732,7 @@ async function loadBootstrapDataWithActiveEsbuild(appRoot?: string): Promise<Boo
|
|
|
716
732
|
diModule,
|
|
717
733
|
searchModule,
|
|
718
734
|
commandLoadersModule,
|
|
735
|
+
webResearchModule,
|
|
719
736
|
commandInterceptorsModule,
|
|
720
737
|
workflowsModule,
|
|
721
738
|
] = await Promise.all([
|
|
@@ -724,6 +741,9 @@ async function loadBootstrapDataWithActiveEsbuild(appRoot?: string): Promise<Boo
|
|
|
724
741
|
compileAndImport(path.join(generatedDir, 'di.generated.ts')),
|
|
725
742
|
loadOptionalGeneratedModule(path.join(generatedDir, 'search.generated.ts'), { searchModuleConfigs: [] }),
|
|
726
743
|
loadOptionalGeneratedModule(path.join(generatedDir, 'command-loaders.generated.ts'), { commandLoaderEntries: [] }),
|
|
744
|
+
loadOptionalGeneratedModule(path.join(generatedDir, 'web-research-adapters.generated.ts'), {
|
|
745
|
+
webResearchAdapterEntries: [],
|
|
746
|
+
}),
|
|
727
747
|
loadOptionalGeneratedModule(path.join(generatedDir, 'command-interceptors.generated.ts'), {
|
|
728
748
|
commandInterceptorEntries: [],
|
|
729
749
|
}),
|
|
@@ -733,7 +753,10 @@ async function loadBootstrapDataWithActiveEsbuild(appRoot?: string): Promise<Boo
|
|
|
733
753
|
return {
|
|
734
754
|
modules: modulesModule.modules as BootstrapData['modules'],
|
|
735
755
|
entities: entitiesModule.entities as BootstrapData['entities'],
|
|
736
|
-
diRegistrars:
|
|
756
|
+
diRegistrars: [
|
|
757
|
+
...(diModule.diRegistrars as BootstrapData['diRegistrars']),
|
|
758
|
+
appValueRegistrar('webResearchAdapterEntries', webResearchModule.webResearchAdapterEntries ?? []),
|
|
759
|
+
],
|
|
737
760
|
entityIds: entityIdsModule.E as BootstrapData['entityIds'],
|
|
738
761
|
// Search configs are needed by workers for indexing
|
|
739
762
|
searchModuleConfigs: (searchModule.searchModuleConfigs ?? []) as BootstrapData['searchModuleConfigs'],
|