@happyvertical/smrt-core 0.40.30 → 0.40.32
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/dist/browser.js +2 -2
- package/dist/consumer-plugin/index.d.ts +5 -0
- package/dist/consumer-plugin/index.d.ts.map +1 -1
- package/dist/consumer-plugin/index.js +6 -38
- package/dist/consumer-plugin/index.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/manifest/static-manifest.js +2 -2
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/test-manifest-stub.js +2 -2
- package/dist/manifest/test-manifest-stub.js.map +1 -1
- package/dist/manifest.json +2 -2
- package/dist/prebuild/index.d.ts.map +1 -1
- package/dist/prebuild/index.js +10 -4
- package/dist/prebuild/index.js.map +1 -1
- package/dist/smrt-knowledge.json +4 -4
- package/dist/system/compatibility.d.ts +12 -0
- package/dist/system/compatibility.d.ts.map +1 -1
- package/dist/system/compatibility.js +51 -2
- package/dist/system/compatibility.js.map +1 -1
- package/dist/system/index.js +2 -2
- package/dist/vite-plugin/api-client-entries.d.ts +17 -0
- package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
- package/dist/vite-plugin/api-client-entries.js +116 -23
- package/dist/vite-plugin/api-client-entries.js.map +1 -1
- package/dist/vite-plugin/generated-client.d.ts +7 -0
- package/dist/vite-plugin/generated-client.d.ts.map +1 -0
- package/dist/vite-plugin/generated-client.js +134 -0
- package/dist/vite-plugin/generated-client.js.map +1 -0
- package/dist/vite-plugin/index.d.ts +5 -0
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +19 -151
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +12 -12
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
- package/dist/vite-plugin/sync-apply-route.js +2 -5
- package/dist/vite-plugin/sync-apply-route.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts +2 -0
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +62 -3
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/dist/vite-plugin.js +2 -2
- package/package.json +4 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AUTO_GENERATED_ROUTE_HEADER } from "./route-header.js";
|
|
2
|
+
import { isCollectionManifestClass } from "./web-collections.js";
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
//#region src/vite-plugin/sync-apply-route.ts
|
|
@@ -65,10 +66,6 @@ function getApiWritableAllowlist(apiConfig) {
|
|
|
65
66
|
const config = getApiConfigObject(apiConfig);
|
|
66
67
|
return Array.isArray(config?.writable) ? config.writable : null;
|
|
67
68
|
}
|
|
68
|
-
/** Mirror of sveltekit-generator.ts `isCollectionClass`. */
|
|
69
|
-
function isCollectionClass(objectDef) {
|
|
70
|
-
return objectDef.extends === "SmrtCollection" || !!objectDef.extendsTypeArg;
|
|
71
|
-
}
|
|
72
69
|
/**
|
|
73
70
|
* Collect the syncable targets from a manifest: non-collection objects whose
|
|
74
71
|
* API config is enabled and exposes at least one mutating action.
|
|
@@ -76,7 +73,7 @@ function isCollectionClass(objectDef) {
|
|
|
76
73
|
function collectSyncApplyTargets(manifest) {
|
|
77
74
|
const targets = [];
|
|
78
75
|
for (const [className, objectDef] of Object.entries(manifest.objects)) {
|
|
79
|
-
if (
|
|
76
|
+
if (isCollectionManifestClass(manifest, objectDef)) continue;
|
|
80
77
|
const apiConfig = objectDef.decoratorConfig?.api;
|
|
81
78
|
if (apiConfig === false) continue;
|
|
82
79
|
const ops = resolveMutatingActions(apiConfig);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-apply-route.js","names":[],"sources":["../../src/vite-plugin/sync-apply-route.ts"],"sourcesContent":["/**\n * SvelteKit generator for the sync-apply batch write route (#1759).\n *\n * Emits ONE route — `{routesDir}/sync/apply/+server.ts` — implementing the\n * shared web/mobile write contract for every syncable model in the manifest\n * (models whose API config exposes at least one of create/update/delete). The\n * generated file is deliberately thin: it maps `object` segments to\n * collections plus per-model policy data, and delegates all batch processing\n * to `processSyncApplyBatch` from `@happyvertical/smrt-core` — the same\n * engine the runtime REST generator uses, so the two transports cannot drift.\n *\n * This lives in its own module (rather than sveltekit-generator.ts) so the\n * shared generator file only needs a one-line registration hook; sibling\n * work (#1757, #1758) touches the same generator files in parallel.\n *\n * Contract documentation: docs/content/architecture/sync-apply-contract.md\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { ApiConfig } from '../registry/types.js';\nimport type {\n SmartObjectDefinition,\n SmartObjectManifest,\n} from '../scanner/types';\nimport { AUTO_GENERATED_ROUTE_HEADER } from './route-header.js';\nimport type { SvelteKitOptions } from './sveltekit-generator.js';\n\nconst MUTATING_ACTIONS = ['create', 'update', 'delete'] as const;\n\ntype MutatingAction = (typeof MUTATING_ACTIONS)[number];\n\n/** Per-model data embedded into the generated route. */\ninterface SyncTargetSpec {\n segment: string;\n /**\n * The manifest registry key, passed VERBATIM to `getCollection()` exactly\n * as the generated CRUD routes do. May be package-qualified\n * (`@happyvertical/smrt-ledgers:Account`) — collapsing it to a simple class\n * name would resolve ambiguously when two loaded packages declare the same\n * simple name.\n */\n registryKey: string;\n ops: MutatingAction[];\n readonlyFields: string[];\n writableAllowlist: string[] | null;\n publicAccess: boolean | 'read';\n tenantScoped: boolean;\n}\n\nfunction getApiConfigObject(apiConfig: unknown): ApiConfig | null {\n if (!apiConfig || typeof apiConfig !== 'object') {\n return null;\n }\n return apiConfig as ApiConfig;\n}\n\n/**\n * Mirror of sveltekit-generator.ts `resolveStandardCrudActions`, narrowed to\n * the mutating subset the sync contract can carry. Kept local so this module\n * needs no exports added to the (parallel-edited) shared generator file.\n */\nfunction resolveMutatingActions(apiConfig: unknown): MutatingAction[] {\n if (apiConfig === false) return [];\n if (\n apiConfig === true ||\n apiConfig === undefined ||\n typeof apiConfig !== 'object'\n ) {\n return [...MUTATING_ACTIONS];\n }\n\n const config = apiConfig as { include?: string[]; exclude?: string[] };\n let actions: MutatingAction[] = Array.isArray(config.include)\n ? MUTATING_ACTIONS.filter((action) => config.include?.includes(action))\n : [...MUTATING_ACTIONS];\n if (Array.isArray(config.exclude)) {\n const excluded = config.exclude;\n actions = actions.filter((action) => !excluded.includes(action));\n }\n return actions;\n}\n\n/** Mirror of sveltekit-generator.ts `getApiPublicAccess` (fail-closed). */\nfunction getApiPublicAccess(apiConfig: unknown): boolean | 'read' {\n const config = getApiConfigObject(apiConfig);\n const value = config?.public;\n if (value === true || value === 'read') {\n return value;\n }\n return false;\n}\n\n/** Mirror of sveltekit-generator.ts `collectReadonlyFieldNames` (#1540, 2b). */\nfunction collectReadonlyFieldNames(objectDef: SmartObjectDefinition): string[] {\n const fields = objectDef.fields || {};\n const names: string[] = [];\n for (const [name, def] of Object.entries(fields)) {\n const meta = (def as { _meta?: Record<string, unknown> })._meta;\n if (\n (def as { readonly?: boolean }).readonly === true ||\n meta?.readonly === true\n ) {\n names.push(name);\n }\n }\n return names;\n}\n\n/** Mirror of sveltekit-generator.ts `getApiWritableAllowlist`. */\nfunction getApiWritableAllowlist(apiConfig: unknown): string[] | null {\n const config = getApiConfigObject(apiConfig);\n return Array.isArray(config?.writable) ? config.writable : null;\n}\n\n/** Mirror of sveltekit-generator.ts `isCollectionClass`. */\nfunction isCollectionClass(objectDef: SmartObjectDefinition): boolean {\n return objectDef.extends === 'SmrtCollection' || !!objectDef.extendsTypeArg;\n}\n\n/**\n * Collect the syncable targets from a manifest: non-collection objects whose\n * API config is enabled and exposes at least one mutating action.\n */\nexport function collectSyncApplyTargets(\n manifest: SmartObjectManifest,\n): SyncTargetSpec[] {\n const targets: SyncTargetSpec[] = [];\n\n for (const [className, objectDef] of Object.entries(manifest.objects)) {\n if (isCollectionClass(objectDef)) continue;\n\n const apiConfig = objectDef.decoratorConfig?.api;\n if (apiConfig === false) continue;\n\n const ops = resolveMutatingActions(apiConfig);\n if (ops.length === 0) continue;\n\n if (!objectDef.collection) continue;\n\n targets.push({\n segment: objectDef.collection,\n registryKey: className,\n ops,\n readonlyFields: collectReadonlyFieldNames(objectDef),\n writableAllowlist: getApiWritableAllowlist(apiConfig),\n publicAccess: getApiPublicAccess(apiConfig),\n tenantScoped: !!objectDef.decoratorConfig?.tenantScoped,\n });\n }\n\n return targets.sort((a, b) => a.segment.localeCompare(b.segment));\n}\n\nfunction serializeTargets(targets: SyncTargetSpec[]): string {\n const entries = targets.map((target) => {\n const fields = [\n `registryKey: ${JSON.stringify(target.registryKey)}`,\n `ops: ${JSON.stringify(target.ops)}`,\n `readonlyFields: ${JSON.stringify(target.readonlyFields)}`,\n `writableAllowlist: ${JSON.stringify(target.writableAllowlist)}`,\n `publicAccess: ${JSON.stringify(target.publicAccess)}`,\n ];\n return ` ${JSON.stringify(target.segment)}: {\\n ${fields.join(',\\n ')},\\n },`;\n });\n return `{\\n${entries.join('\\n')}\\n}`;\n}\n\n/**\n * Render the generated `+server.ts` content for the sync-apply route.\n * Exported for tests.\n */\nexport function generateSyncApplyRouteTemplate(\n targets: SyncTargetSpec[],\n): string {\n const anyTenantScoped = targets.some((target) => target.tenantScoped);\n\n const tenantHelper = anyTenantScoped\n ? `\nimport { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenancy';\n\nfunction establishTenantContext(locals: unknown): void {\n if (hasTenantContext()) return;\n if (!locals || typeof locals !== 'object') return;\n const l = locals as Record<string, unknown>;\n const user = l.user as Record<string, unknown> | undefined;\n const session = l.session as Record<string, unknown> | undefined;\n const tenantId = l.tenantId ?? user?.tenantId ?? session?.tenantId;\n if (typeof tenantId === 'string' && tenantId) {\n enterTenantContext({ tenantId });\n }\n}\n`\n : '';\n\n return `${AUTO_GENERATED_ROUTE_HEADER}\n// DO NOT EDIT - changes will be overwritten\n//\n// Idempotent sync-apply batch write contract (#1759) — the shared write path\n// for the web offline outbox (#1762) and the KMP mobile write queue (#1739).\n// Contract: docs/content/architecture/sync-apply-contract.md\n\nimport {\n applySyncWritablePolicy,\n processSyncApplyBatch,\n type SyncApplyOp,\n type SyncApplyTarget,\n} from '@happyvertical/smrt-core';\nimport { json } from '@sveltejs/kit';\nimport { getCollection } from '$lib/server/smrt';\nimport type { RequestHandler } from './$types';\n${tenantHelper}\ninterface SyncTargetConfig {\n /** Manifest registry key (may be package-qualified), as CRUD routes use. */\n registryKey: string;\n ops: SyncApplyOp[];\n readonlyFields: string[];\n writableAllowlist: string[] | null;\n publicAccess: boolean | 'read';\n}\n\n// One entry per syncable model, keyed by the collection route segment its\n// generated CRUD routes use.\nconst SYNC_TARGETS: Record<string, SyncTargetConfig> = ${serializeTargets(targets)};\n\n// Fail-closed authorization (#1540): sync mutations require an authenticated\n// principal on \\`locals\\` unless the target model is \\`api: { public: true }\\`.\nfunction hasAuthenticatedPrincipal(locals: unknown): boolean {\n if (!locals || typeof locals !== 'object') return false;\n const l = locals as Record<string, unknown>;\n const isResolvedPrincipal = (v: unknown) =>\n typeof v === 'object' && v !== null;\n return (\n isResolvedPrincipal(l.user) ||\n isResolvedPrincipal(l.session) ||\n l.smrtAuth === true\n );\n}\n\nexport const POST: RequestHandler = async ({ locals, request }) => {\n const authenticated = hasAuthenticatedPrincipal(locals);\n${anyTenantScoped ? ' establishTenantContext(locals);\\n' : ''}\n let body: unknown;\n try {\n body = await request.json();\n } catch {\n return json(\n { error: { code: 'invalid_batch', message: 'Invalid JSON body' } },\n { status: 400 },\n );\n }\n\n const outcome = await processSyncApplyBatch(body, {\n resolveTarget: async (segment: string): Promise<SyncApplyTarget | null> => {\n const target = SYNC_TARGETS[segment];\n if (!target) return null;\n // The verbatim manifest key, exactly as the generated CRUD routes pass\n // it — package-qualified keys stay qualified so same-simple-name\n // classes across packages resolve unambiguously.\n const collection = await getCollection(target.registryKey);\n return {\n objectName: target.registryKey,\n collection,\n isOpAllowed: (op: SyncApplyOp) => target.ops.includes(op),\n authorize: () =>\n authenticated || target.publicAccess === true\n ? ('ok' as const)\n : ('auth_required' as const),\n prepare: (payload: Record<string, unknown>) =>\n applySyncWritablePolicy(payload, {\n readonlyFields: target.readonlyFields,\n writableAllowlist: target.writableAllowlist,\n }),\n };\n },\n });\n\n return json(outcome.body, { status: outcome.status });\n};\n`;\n}\n\n/**\n * Generate `{routesDir}/sync/apply/+server.ts` for the manifest's syncable\n * models. Returns true when a route was written. Skips generation (with a\n * warning) if a non-generated file already occupies the path, and writes\n * nothing when the manifest has no syncable models.\n */\nexport function generateSyncApplyRoute(\n projectRoot: string,\n manifest: SmartObjectManifest,\n options: SvelteKitOptions,\n): boolean {\n const targets = collectSyncApplyTargets(manifest);\n if (targets.length === 0) {\n return false;\n }\n\n const routeDir = join(projectRoot, options.routesDir, 'sync', 'apply');\n const routePath = join(routeDir, '+server.ts');\n\n if (existsSync(routePath)) {\n const existing = readFileSync(routePath, 'utf-8');\n if (!existing.startsWith(AUTO_GENERATED_ROUTE_HEADER)) {\n console.warn(\n `[smrt] Skipping sync-apply route - ${routePath} exists and is not generated`,\n );\n return false;\n }\n }\n\n if (!existsSync(routeDir)) {\n mkdirSync(routeDir, { recursive: true });\n }\n\n writeFileSync(routePath, generateSyncApplyRouteTemplate(targets), 'utf-8');\n console.log(`[smrt] Generated: ${routePath}`);\n return true;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA4BA,IAAM,mBAAmB;CAAC;CAAU;CAAU;AAAQ;AAsBtD,SAAS,mBAAmB,WAAsC;CAChE,IAAI,CAAC,aAAa,OAAO,cAAc,UACrC,OAAO;CAET,OAAO;AACT;;;;;;AAOA,SAAS,uBAAuB,WAAsC;CACpE,IAAI,cAAc,OAAO,OAAO,CAAC;CACjC,IACE,cAAc,QACd,cAAc,KAAA,KACd,OAAO,cAAc,UAErB,OAAO,CAAC,GAAG,gBAAgB;CAG7B,MAAM,SAAS;CACf,IAAI,UAA4B,MAAM,QAAQ,OAAO,OAAO,IACxD,iBAAiB,QAAQ,WAAW,OAAO,SAAS,SAAS,MAAM,CAAC,IACpE,CAAC,GAAG,gBAAgB;CACxB,IAAI,MAAM,QAAQ,OAAO,OAAO,GAAG;EACjC,MAAM,WAAW,OAAO;EACxB,UAAU,QAAQ,QAAQ,WAAW,CAAC,SAAS,SAAS,MAAM,CAAC;CACjE;CACA,OAAO;AACT;;AAGA,SAAS,mBAAmB,WAAsC;CAEhE,MAAM,QADS,mBAAmB,SACpB,CAAA,EAAQ;CACtB,IAAI,UAAU,QAAQ,UAAU,QAC9B,OAAO;CAET,OAAO;AACT;;AAGA,SAAS,0BAA0B,WAA4C;CAC7E,MAAM,SAAS,UAAU,UAAU,CAAC;CACpC,MAAM,QAAkB,CAAC;CACzB,KAAK,MAAM,CAAC,MAAM,QAAQ,OAAO,QAAQ,MAAM,GAAG;EAChD,MAAM,OAAQ,IAA4C;EAC1D,IACG,IAA+B,aAAa,QAC7C,MAAM,aAAa,MAEnB,MAAM,KAAK,IAAI;CAEnB;CACA,OAAO;AACT;;AAGA,SAAS,wBAAwB,WAAqC;CACpE,MAAM,SAAS,mBAAmB,SAAS;CAC3C,OAAO,MAAM,QAAQ,QAAQ,QAAQ,IAAI,OAAO,WAAW;AAC7D;;AAGA,SAAS,kBAAkB,WAA2C;CACpE,OAAO,UAAU,YAAY,oBAAoB,CAAC,CAAC,UAAU;AAC/D;;;;;AAMA,SAAgB,wBACd,UACkB;CAClB,MAAM,UAA4B,CAAC;CAEnC,KAAK,MAAM,CAAC,WAAW,cAAc,OAAO,QAAQ,SAAS,OAAO,GAAG;EACrE,IAAI,kBAAkB,SAAS,GAAG;EAElC,MAAM,YAAY,UAAU,iBAAiB;EAC7C,IAAI,cAAc,OAAO;EAEzB,MAAM,MAAM,uBAAuB,SAAS;EAC5C,IAAI,IAAI,WAAW,GAAG;EAEtB,IAAI,CAAC,UAAU,YAAY;EAE3B,QAAQ,KAAK;GACX,SAAS,UAAU;GACnB,aAAa;GACb;GACA,gBAAgB,0BAA0B,SAAS;GACnD,mBAAmB,wBAAwB,SAAS;GACpD,cAAc,mBAAmB,SAAS;GAC1C,cAAc,CAAC,CAAC,UAAU,iBAAiB;EAC7C,CAAC;CACH;CAEA,OAAO,QAAQ,MAAM,GAAG,MAAM,EAAE,QAAQ,cAAc,EAAE,OAAO,CAAC;AAClE;AAEA,SAAS,iBAAiB,SAAmC;CAW3D,OAAO,MAVS,QAAQ,KAAK,WAAW;EACtC,MAAM,SAAS;GACb,gBAAgB,KAAK,UAAU,OAAO,WAAW;GACjD,QAAQ,KAAK,UAAU,OAAO,GAAG;GACjC,mBAAmB,KAAK,UAAU,OAAO,cAAc;GACvD,sBAAsB,KAAK,UAAU,OAAO,iBAAiB;GAC7D,iBAAiB,KAAK,UAAU,OAAO,YAAY;EACrD;EACA,OAAO,KAAK,KAAK,UAAU,OAAO,OAAO,EAAE,WAAW,OAAO,KAAK,SAAS,EAAE;CAC/E,CACa,CAAA,CAAQ,KAAK,IAAI,EAAE;AAClC;;;;;AAMA,SAAgB,+BACd,SACQ;CACR,MAAM,kBAAkB,QAAQ,MAAM,WAAW,OAAO,YAAY;CAoBpE,OAAO,GAAG,4BAA4B;;;;;;;;;;;;;;;;EAlBjB,kBACjB;;;;;;;;;;;;;;IAeA,GAkBS;;;;;;;;;;;;yDAY0C,iBAAiB,OAAO,EAAE;;;;;;;;;;;;;;;;;;EAkBjF,kBAAkB,wCAAwC,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuC/D;;;;;;;AAQA,SAAgB,uBACd,aACA,UACA,SACS;CACT,MAAM,UAAU,wBAAwB,QAAQ;CAChD,IAAI,QAAQ,WAAW,GACrB,OAAO;CAGT,MAAM,WAAW,KAAK,aAAa,QAAQ,WAAW,QAAQ,OAAO;CACrE,MAAM,YAAY,KAAK,UAAU,YAAY;CAE7C,IAAI,WAAW,SAAS;MAElB,CADa,aAAa,WAAW,OACpC,CAAA,CAAS,WAAA,6CAAsC,GAAG;GACrD,QAAQ,KACN,sCAAsC,UAAU,6BAClD;GACA,OAAO;EACT;;CAGF,IAAI,CAAC,WAAW,QAAQ,GACtB,UAAU,UAAU,EAAE,WAAW,KAAK,CAAC;CAGzC,cAAc,WAAW,+BAA+B,OAAO,GAAG,OAAO;CACzE,QAAQ,IAAI,qBAAqB,WAAW;CAC5C,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"sync-apply-route.js","names":[],"sources":["../../src/vite-plugin/sync-apply-route.ts"],"sourcesContent":["/**\n * SvelteKit generator for the sync-apply batch write route (#1759).\n *\n * Emits ONE route — `{routesDir}/sync/apply/+server.ts` — implementing the\n * shared web/mobile write contract for every syncable model in the manifest\n * (models whose API config exposes at least one of create/update/delete). The\n * generated file is deliberately thin: it maps `object` segments to\n * collections plus per-model policy data, and delegates all batch processing\n * to `processSyncApplyBatch` from `@happyvertical/smrt-core` — the same\n * engine the runtime REST generator uses, so the two transports cannot drift.\n *\n * This lives in its own module (rather than sveltekit-generator.ts) so the\n * shared generator file only needs a one-line registration hook; sibling\n * work (#1757, #1758) touches the same generator files in parallel.\n *\n * Contract documentation: docs/content/architecture/sync-apply-contract.md\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { ApiConfig } from '../registry/types.js';\nimport type {\n SmartObjectDefinition,\n SmartObjectManifest,\n} from '../scanner/types';\nimport { AUTO_GENERATED_ROUTE_HEADER } from './route-header.js';\nimport type { SvelteKitOptions } from './sveltekit-generator.js';\nimport { isCollectionManifestClass } from './web-collections.js';\n\nconst MUTATING_ACTIONS = ['create', 'update', 'delete'] as const;\n\ntype MutatingAction = (typeof MUTATING_ACTIONS)[number];\n\n/** Per-model data embedded into the generated route. */\ninterface SyncTargetSpec {\n segment: string;\n /**\n * The manifest registry key, passed VERBATIM to `getCollection()` exactly\n * as the generated CRUD routes do. May be package-qualified\n * (`@happyvertical/smrt-ledgers:Account`) — collapsing it to a simple class\n * name would resolve ambiguously when two loaded packages declare the same\n * simple name.\n */\n registryKey: string;\n ops: MutatingAction[];\n readonlyFields: string[];\n writableAllowlist: string[] | null;\n publicAccess: boolean | 'read';\n tenantScoped: boolean;\n}\n\nfunction getApiConfigObject(apiConfig: unknown): ApiConfig | null {\n if (!apiConfig || typeof apiConfig !== 'object') {\n return null;\n }\n return apiConfig as ApiConfig;\n}\n\n/**\n * Mirror of sveltekit-generator.ts `resolveStandardCrudActions`, narrowed to\n * the mutating subset the sync contract can carry. Kept local so this module\n * needs no exports added to the (parallel-edited) shared generator file.\n */\nfunction resolveMutatingActions(apiConfig: unknown): MutatingAction[] {\n if (apiConfig === false) return [];\n if (\n apiConfig === true ||\n apiConfig === undefined ||\n typeof apiConfig !== 'object'\n ) {\n return [...MUTATING_ACTIONS];\n }\n\n const config = apiConfig as { include?: string[]; exclude?: string[] };\n let actions: MutatingAction[] = Array.isArray(config.include)\n ? MUTATING_ACTIONS.filter((action) => config.include?.includes(action))\n : [...MUTATING_ACTIONS];\n if (Array.isArray(config.exclude)) {\n const excluded = config.exclude;\n actions = actions.filter((action) => !excluded.includes(action));\n }\n return actions;\n}\n\n/** Mirror of sveltekit-generator.ts `getApiPublicAccess` (fail-closed). */\nfunction getApiPublicAccess(apiConfig: unknown): boolean | 'read' {\n const config = getApiConfigObject(apiConfig);\n const value = config?.public;\n if (value === true || value === 'read') {\n return value;\n }\n return false;\n}\n\n/** Mirror of sveltekit-generator.ts `collectReadonlyFieldNames` (#1540, 2b). */\nfunction collectReadonlyFieldNames(objectDef: SmartObjectDefinition): string[] {\n const fields = objectDef.fields || {};\n const names: string[] = [];\n for (const [name, def] of Object.entries(fields)) {\n const meta = (def as { _meta?: Record<string, unknown> })._meta;\n if (\n (def as { readonly?: boolean }).readonly === true ||\n meta?.readonly === true\n ) {\n names.push(name);\n }\n }\n return names;\n}\n\n/** Mirror of sveltekit-generator.ts `getApiWritableAllowlist`. */\nfunction getApiWritableAllowlist(apiConfig: unknown): string[] | null {\n const config = getApiConfigObject(apiConfig);\n return Array.isArray(config?.writable) ? config.writable : null;\n}\n\n/**\n * Collect the syncable targets from a manifest: non-collection objects whose\n * API config is enabled and exposes at least one mutating action.\n */\nexport function collectSyncApplyTargets(\n manifest: SmartObjectManifest,\n): SyncTargetSpec[] {\n const targets: SyncTargetSpec[] = [];\n\n for (const [className, objectDef] of Object.entries(manifest.objects)) {\n if (isCollectionManifestClass(manifest, objectDef)) continue;\n\n const apiConfig = objectDef.decoratorConfig?.api;\n if (apiConfig === false) continue;\n\n const ops = resolveMutatingActions(apiConfig);\n if (ops.length === 0) continue;\n\n if (!objectDef.collection) continue;\n\n targets.push({\n segment: objectDef.collection,\n registryKey: className,\n ops,\n readonlyFields: collectReadonlyFieldNames(objectDef),\n writableAllowlist: getApiWritableAllowlist(apiConfig),\n publicAccess: getApiPublicAccess(apiConfig),\n tenantScoped: !!objectDef.decoratorConfig?.tenantScoped,\n });\n }\n\n return targets.sort((a, b) => a.segment.localeCompare(b.segment));\n}\n\nfunction serializeTargets(targets: SyncTargetSpec[]): string {\n const entries = targets.map((target) => {\n const fields = [\n `registryKey: ${JSON.stringify(target.registryKey)}`,\n `ops: ${JSON.stringify(target.ops)}`,\n `readonlyFields: ${JSON.stringify(target.readonlyFields)}`,\n `writableAllowlist: ${JSON.stringify(target.writableAllowlist)}`,\n `publicAccess: ${JSON.stringify(target.publicAccess)}`,\n ];\n return ` ${JSON.stringify(target.segment)}: {\\n ${fields.join(',\\n ')},\\n },`;\n });\n return `{\\n${entries.join('\\n')}\\n}`;\n}\n\n/**\n * Render the generated `+server.ts` content for the sync-apply route.\n * Exported for tests.\n */\nexport function generateSyncApplyRouteTemplate(\n targets: SyncTargetSpec[],\n): string {\n const anyTenantScoped = targets.some((target) => target.tenantScoped);\n\n const tenantHelper = anyTenantScoped\n ? `\nimport { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenancy';\n\nfunction establishTenantContext(locals: unknown): void {\n if (hasTenantContext()) return;\n if (!locals || typeof locals !== 'object') return;\n const l = locals as Record<string, unknown>;\n const user = l.user as Record<string, unknown> | undefined;\n const session = l.session as Record<string, unknown> | undefined;\n const tenantId = l.tenantId ?? user?.tenantId ?? session?.tenantId;\n if (typeof tenantId === 'string' && tenantId) {\n enterTenantContext({ tenantId });\n }\n}\n`\n : '';\n\n return `${AUTO_GENERATED_ROUTE_HEADER}\n// DO NOT EDIT - changes will be overwritten\n//\n// Idempotent sync-apply batch write contract (#1759) — the shared write path\n// for the web offline outbox (#1762) and the KMP mobile write queue (#1739).\n// Contract: docs/content/architecture/sync-apply-contract.md\n\nimport {\n applySyncWritablePolicy,\n processSyncApplyBatch,\n type SyncApplyOp,\n type SyncApplyTarget,\n} from '@happyvertical/smrt-core';\nimport { json } from '@sveltejs/kit';\nimport { getCollection } from '$lib/server/smrt';\nimport type { RequestHandler } from './$types';\n${tenantHelper}\ninterface SyncTargetConfig {\n /** Manifest registry key (may be package-qualified), as CRUD routes use. */\n registryKey: string;\n ops: SyncApplyOp[];\n readonlyFields: string[];\n writableAllowlist: string[] | null;\n publicAccess: boolean | 'read';\n}\n\n// One entry per syncable model, keyed by the collection route segment its\n// generated CRUD routes use.\nconst SYNC_TARGETS: Record<string, SyncTargetConfig> = ${serializeTargets(targets)};\n\n// Fail-closed authorization (#1540): sync mutations require an authenticated\n// principal on \\`locals\\` unless the target model is \\`api: { public: true }\\`.\nfunction hasAuthenticatedPrincipal(locals: unknown): boolean {\n if (!locals || typeof locals !== 'object') return false;\n const l = locals as Record<string, unknown>;\n const isResolvedPrincipal = (v: unknown) =>\n typeof v === 'object' && v !== null;\n return (\n isResolvedPrincipal(l.user) ||\n isResolvedPrincipal(l.session) ||\n l.smrtAuth === true\n );\n}\n\nexport const POST: RequestHandler = async ({ locals, request }) => {\n const authenticated = hasAuthenticatedPrincipal(locals);\n${anyTenantScoped ? ' establishTenantContext(locals);\\n' : ''}\n let body: unknown;\n try {\n body = await request.json();\n } catch {\n return json(\n { error: { code: 'invalid_batch', message: 'Invalid JSON body' } },\n { status: 400 },\n );\n }\n\n const outcome = await processSyncApplyBatch(body, {\n resolveTarget: async (segment: string): Promise<SyncApplyTarget | null> => {\n const target = SYNC_TARGETS[segment];\n if (!target) return null;\n // The verbatim manifest key, exactly as the generated CRUD routes pass\n // it — package-qualified keys stay qualified so same-simple-name\n // classes across packages resolve unambiguously.\n const collection = await getCollection(target.registryKey);\n return {\n objectName: target.registryKey,\n collection,\n isOpAllowed: (op: SyncApplyOp) => target.ops.includes(op),\n authorize: () =>\n authenticated || target.publicAccess === true\n ? ('ok' as const)\n : ('auth_required' as const),\n prepare: (payload: Record<string, unknown>) =>\n applySyncWritablePolicy(payload, {\n readonlyFields: target.readonlyFields,\n writableAllowlist: target.writableAllowlist,\n }),\n };\n },\n });\n\n return json(outcome.body, { status: outcome.status });\n};\n`;\n}\n\n/**\n * Generate `{routesDir}/sync/apply/+server.ts` for the manifest's syncable\n * models. Returns true when a route was written. Skips generation (with a\n * warning) if a non-generated file already occupies the path, and writes\n * nothing when the manifest has no syncable models.\n */\nexport function generateSyncApplyRoute(\n projectRoot: string,\n manifest: SmartObjectManifest,\n options: SvelteKitOptions,\n): boolean {\n const targets = collectSyncApplyTargets(manifest);\n if (targets.length === 0) {\n return false;\n }\n\n const routeDir = join(projectRoot, options.routesDir, 'sync', 'apply');\n const routePath = join(routeDir, '+server.ts');\n\n if (existsSync(routePath)) {\n const existing = readFileSync(routePath, 'utf-8');\n if (!existing.startsWith(AUTO_GENERATED_ROUTE_HEADER)) {\n console.warn(\n `[smrt] Skipping sync-apply route - ${routePath} exists and is not generated`,\n );\n return false;\n }\n }\n\n if (!existsSync(routeDir)) {\n mkdirSync(routeDir, { recursive: true });\n }\n\n writeFileSync(routePath, generateSyncApplyRouteTemplate(targets), 'utf-8');\n console.log(`[smrt] Generated: ${routePath}`);\n return true;\n}\n"],"mappings":"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"}
|
|
@@ -53,6 +53,8 @@ export declare function findManifestObjectByName(manifest: SmartObjectManifest,
|
|
|
53
53
|
* would be mistaken for a model and claim its base model's REST collection.
|
|
54
54
|
*/
|
|
55
55
|
export declare function isCollectionManifestClass(manifest: SmartObjectManifest, obj: SmartObjectDefinition, seen?: Set<string>): boolean;
|
|
56
|
+
export declare function resolveCollectionItemTypeName(manifest: SmartObjectManifest, obj: SmartObjectDefinition): string | undefined;
|
|
57
|
+
export declare function resolveCollectionItemObject(manifest: SmartObjectManifest, obj: SmartObjectDefinition): SmartObjectDefinition | undefined;
|
|
56
58
|
/**
|
|
57
59
|
* Select the manifest entries that become web collection definitions: one per
|
|
58
60
|
* REST collection, STI children folding into their base model, collection
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-collections.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/web-collections.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EACV,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,qBAAqB,CAAC;AAyB7B,4EAA4E;AAC5E,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;IAC9B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,iEAAiE;AACjE,MAAM,MAAM,mBAAmB,GAC3B,YAAY,GACZ,iBAAiB,GACjB,WAAW,GACX,YAAY,CAAC;AAEjB;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,IAAI,EAAE,mBAAmB,CAAC;IAC1B,oEAAoE;IACpE,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,2EAA2E;AAC3E,MAAM,WAAW,kBAAkB;IACjC,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,GAAG,EAAE,qBAAqB,CAAC;IAC3B,mDAAmD;IACnD,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;
|
|
1
|
+
{"version":3,"file":"web-collections.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/web-collections.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EACV,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,qBAAqB,CAAC;AAyB7B,4EAA4E;AAC5E,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;IAC9B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,iEAAiE;AACjE,MAAM,MAAM,mBAAmB,GAC3B,YAAY,GACZ,iBAAiB,GACjB,WAAW,GACX,YAAY,CAAC;AAEjB;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,IAAI,EAAE,mBAAmB,CAAC;IAC1B,oEAAoE;IACpE,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,2EAA2E;AAC3E,MAAM,WAAW,kBAAkB;IACjC,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,GAAG,EAAE,qBAAqB,CAAC;IAC3B,mDAAmD;IACnD,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AA8FD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,mBAAmB,EAC7B,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,qBAAqB,GAC5B,qBAAqB,GAAG,SAAS,CAanC;AAsBD;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,mBAAmB,EAC7B,GAAG,EAAE,qBAAqB,EAC1B,IAAI,GAAE,GAAG,CAAC,MAAM,CAAa,GAC5B,OAAO,CAYT;AA+FD,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,mBAAmB,EAC7B,GAAG,EAAE,qBAAqB,GACzB,MAAM,GAAG,SAAS,CAEpB;AAED,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,mBAAmB,EAC7B,GAAG,EAAE,qBAAqB,GACzB,qBAAqB,GAAG,SAAS,CAEnC;AAsED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,mBAAmB,GAC5B,kBAAkB,EAAE,CAEtB;AAqBD;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,kBAAkB,EACzB,QAAQ,EAAE,mBAAmB,GAC5B;IACD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC3C,aAAa,EAAE,eAAe,EAAE,CAAC;CAClC,CAUA;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,qBAAqB,GACzB,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAcpC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,qBAAqB,EAC1B,QAAQ,EAAE,mBAAmB,GAC5B,eAAe,EAAE,CAsCnB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,kBAAkB,GACxB,cAAc,EAAE,CAelB;AAyBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,MAAM,CAa5E"}
|
|
@@ -61,7 +61,8 @@ function getManifestObjectIndex(manifest) {
|
|
|
61
61
|
const index = {
|
|
62
62
|
byExactName: /* @__PURE__ */ new Map(),
|
|
63
63
|
byPackageAndClass: /* @__PURE__ */ new Map(),
|
|
64
|
-
bySimpleName: /* @__PURE__ */ new Map()
|
|
64
|
+
bySimpleName: /* @__PURE__ */ new Map(),
|
|
65
|
+
packageByObject: /* @__PURE__ */ new WeakMap()
|
|
65
66
|
};
|
|
66
67
|
for (const [manifestKey, candidate] of entries) {
|
|
67
68
|
if (!index.byExactName.has(manifestKey)) index.byExactName.set(manifestKey, candidate);
|
|
@@ -69,6 +70,7 @@ function getManifestObjectIndex(manifest) {
|
|
|
69
70
|
if (!index.bySimpleName.has(candidate.className)) index.bySimpleName.set(candidate.className, candidate);
|
|
70
71
|
const packageName = manifestObjectPackage(manifestKey, candidate);
|
|
71
72
|
if (packageName) {
|
|
73
|
+
if (!index.packageByObject.has(candidate)) index.packageByObject.set(candidate, packageName);
|
|
72
74
|
const packageKey = packageAndClassKey(packageName, candidate.className);
|
|
73
75
|
if (!index.byPackageAndClass.has(packageKey)) index.byPackageAndClass.set(packageKey, candidate);
|
|
74
76
|
}
|
|
@@ -76,6 +78,9 @@ function getManifestObjectIndex(manifest) {
|
|
|
76
78
|
manifestObjectIndexes.set(manifest.objects, index);
|
|
77
79
|
return index;
|
|
78
80
|
}
|
|
81
|
+
function indexedManifestObjectPackage(manifest, obj) {
|
|
82
|
+
return getManifestObjectIndex(manifest).packageByObject.get(obj) || manifestObjectPackage(void 0, obj);
|
|
83
|
+
}
|
|
79
84
|
/**
|
|
80
85
|
* Resolve a manifest object deterministically across aggregated packages.
|
|
81
86
|
*
|
|
@@ -88,7 +93,7 @@ function findManifestObjectByName(manifest, name, owner) {
|
|
|
88
93
|
const index = getManifestObjectIndex(manifest);
|
|
89
94
|
const exact = name.includes(":") ? index.byExactName.get(name) : void 0;
|
|
90
95
|
if (exact) return exact;
|
|
91
|
-
const ownerPackage = owner ?
|
|
96
|
+
const ownerPackage = owner ? indexedManifestObjectPackage(manifest, owner) : void 0;
|
|
92
97
|
return (ownerPackage ? index.byPackageAndClass.get(packageAndClassKey(ownerPackage, name)) : void 0) || index.bySimpleName.get(name);
|
|
93
98
|
}
|
|
94
99
|
/**
|
|
@@ -130,6 +135,60 @@ function isCollectionManifestClass(manifest, obj, seen = /* @__PURE__ */ new Set
|
|
|
130
135
|
return parent ? isCollectionManifestClass(manifest, parent, seen) : false;
|
|
131
136
|
}
|
|
132
137
|
/**
|
|
138
|
+
* Resolve the row model carried by a collection class.
|
|
139
|
+
*
|
|
140
|
+
* An explicit generic on any ancestor is authoritative. Conventional
|
|
141
|
+
* `FooCollection` → `Foo` lookup is only a fallback after the full ancestry
|
|
142
|
+
* has been checked, so an unrelated `SpecialFoo` cannot override an inherited
|
|
143
|
+
* `FooCollection<Foo>` contract.
|
|
144
|
+
*/
|
|
145
|
+
function collectionAncestry(manifest, obj) {
|
|
146
|
+
const ancestry = [];
|
|
147
|
+
const seen = /* @__PURE__ */ new Set();
|
|
148
|
+
let candidate = obj;
|
|
149
|
+
while (candidate) {
|
|
150
|
+
ancestry.push(candidate);
|
|
151
|
+
const parentName = candidate.extendsQualified || candidate.extends;
|
|
152
|
+
if (!parentName || seen.has(parentName)) break;
|
|
153
|
+
seen.add(parentName);
|
|
154
|
+
candidate = findManifestObjectByName(manifest, parentName, candidate);
|
|
155
|
+
}
|
|
156
|
+
return ancestry;
|
|
157
|
+
}
|
|
158
|
+
function resolveCollectionItemCandidate(manifest, name, owner) {
|
|
159
|
+
const object = findManifestObjectByName(manifest, name, owner);
|
|
160
|
+
const ownerPackage = indexedManifestObjectPackage(manifest, owner);
|
|
161
|
+
if (!name.includes(":") && ownerPackage) {
|
|
162
|
+
if ((object ? indexedManifestObjectPackage(manifest, object) : void 0) !== ownerPackage) return {
|
|
163
|
+
name: `${ownerPackage}:${name}`,
|
|
164
|
+
owner
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
name,
|
|
169
|
+
owner,
|
|
170
|
+
object
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function resolveCollectionItemReference(manifest, obj) {
|
|
174
|
+
const ancestry = collectionAncestry(manifest, obj);
|
|
175
|
+
for (const collectionClass of ancestry) if (collectionClass.extendsTypeArg) return resolveCollectionItemCandidate(manifest, collectionClass.extendsTypeArg, collectionClass);
|
|
176
|
+
let fallback;
|
|
177
|
+
for (const collectionClass of ancestry) {
|
|
178
|
+
if (!collectionClass.className.endsWith("Collection")) continue;
|
|
179
|
+
const candidate = resolveCollectionItemCandidate(manifest, collectionClass.className.slice(0, -10), collectionClass);
|
|
180
|
+
if (candidate.object) return candidate;
|
|
181
|
+
fallback ??= candidate;
|
|
182
|
+
}
|
|
183
|
+
return fallback;
|
|
184
|
+
}
|
|
185
|
+
function resolveCollectionItemTypeName(manifest, obj) {
|
|
186
|
+
return resolveCollectionItemReference(manifest, obj)?.name;
|
|
187
|
+
}
|
|
188
|
+
function resolveCollectionItemObject(manifest, obj) {
|
|
189
|
+
return resolveCollectionItemReference(manifest, obj)?.object;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
133
192
|
* True when some ancestor model maps to the SAME REST collection (a shared STI
|
|
134
193
|
* table). The STI base model owns the shared table's single definition.
|
|
135
194
|
*/
|
|
@@ -369,6 +428,6 @@ function computeWebManifestHash(manifest) {
|
|
|
369
428
|
return createHash("sha256").update(canonicalJson).digest("base64url").slice(0, 16);
|
|
370
429
|
}
|
|
371
430
|
//#endregion
|
|
372
|
-
export { buildWebCollectionDefinition, buildWebFieldDefinitions, buildWebRelationships, buildWebToolDescriptors, computeWebManifestHash, findManifestObjectByName, isCollectionManifestClass, selectWebCollectionEntries };
|
|
431
|
+
export { buildWebCollectionDefinition, buildWebFieldDefinitions, buildWebRelationships, buildWebToolDescriptors, computeWebManifestHash, findManifestObjectByName, isCollectionManifestClass, resolveCollectionItemObject, resolveCollectionItemTypeName, selectWebCollectionEntries };
|
|
373
432
|
|
|
374
433
|
//# sourceMappingURL=web-collections.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-collections.js","names":[],"sources":["../../src/vite-plugin/web-collections.ts"],"sourcesContent":["/**\n * Manifest → web collection selection (single source of truth).\n *\n * The browser client data runtime (`@happyvertical/smrt-web`, #1761) consumes\n * one typed collection definition per API-exposed REST collection. Three\n * emission sites need the SAME selection and field rules, or the emitted\n * runtime values and their declared types drift apart:\n *\n * - the `\\0smrt:web` runtime virtual module (JSON literal — {@link generateWebModule})\n * - the `@happyvertical/smrt-virt-web` ambient d.ts (vite-plugin)\n * - the physical `@smrt/web` d.ts (prebuild, for `tsc`-only consumers)\n *\n * All three import {@link selectWebCollectionEntries} from here so the value\n * emission and the type emission can never disagree. The per-collection SHAPE\n * (name/className/endpoint/idField/actions/fields/relationships) is built by the\n * ONE {@link buildWebCollectionDefinition}, shared by the runtime emission AND\n * the #1764 {@link computeWebManifestHash} shape digest — so the emitted shape\n * and the hashed shape can never drift (a drift would let the hash under-cover a\n * change → stale client caches).\n */\n\nimport { createHash } from 'node:crypto';\nimport {\n buildToolDescriptors,\n type ToolDescriptor,\n type ToolFieldMeta,\n} from '../generators/tool-schema.js';\nimport type {\n FieldDefinition,\n SmartObjectDefinition,\n SmartObjectManifest,\n} from '../scanner/types.js';\nimport { resolveApiActionSet } from './sveltekit-generator.js';\n\n/**\n * Field types that are relationship pseudo-columns rather than persisted\n * public-DTO columns — they never appear on the wire as scalar values.\n */\nconst RELATIONSHIP_FIELD_TYPES: ReadonlySet<FieldDefinition['type']> = new Set([\n 'oneToMany',\n 'manyToMany',\n]);\n\n/**\n * Field types that describe a relationship to another model. A superset of\n * {@link RELATIONSHIP_FIELD_TYPES}: `foreignKey`/`crossPackageRef` are persisted\n * scalar id columns (they DO appear on the wire), while `oneToMany`/`manyToMany`\n * are pseudo-columns — but all four carry a `related` edge to a sibling model.\n */\nconst RELATIONSHIP_EDGE_TYPES: ReadonlySet<FieldDefinition['type']> = new Set([\n 'foreignKey',\n 'crossPackageRef',\n 'oneToMany',\n 'manyToMany',\n]);\n\n/** Informational per-column metadata carried by a collection definition. */\nexport interface WebFieldDefinition {\n type: FieldDefinition['type'];\n required?: boolean;\n default?: unknown;\n}\n\n/** The relationship kinds a web collection edge can describe. */\nexport type WebRelationshipKind =\n | 'foreignKey'\n | 'crossPackageRef'\n | 'oneToMany'\n | 'manyToMany';\n\n/**\n * One manifest-derived relationship edge from a collection to a sibling REST\n * collection. Consumed by the browser client-data runtime to invalidate\n * dependent collection caches when this collection is mutated (#1761) — the\n * cache-invalidation graph is derived entirely from these edges, never\n * hand-wired. SMRT-owned data: no client-engine type appears here.\n */\nexport interface WebRelationship {\n /** The declaring field carrying the relationship (e.g. `groupId`, `items`). */\n field: string;\n /** The relationship kind, mirroring the manifest field type. */\n kind: WebRelationshipKind;\n /** REST collection name the edge resolves to (e.g. `ad_groups`). */\n relatedCollection: string;\n}\n\n/** One selected REST collection and the model that owns its definition. */\nexport interface WebCollectionEntry {\n /** REST collection name (pluralized), e.g. `products`. */\n collection: string;\n /** The manifest object that owns this collection's definition. */\n obj: SmartObjectDefinition;\n /** Sorted set of exposed CRUD + custom actions. */\n actions: string[];\n}\n\nfunction compareText(left: string, right: string): number {\n if (left < right) return -1;\n if (left > right) return 1;\n return 0;\n}\n\nfunction manifestObjectPackage(\n manifestKey: string | undefined,\n obj: SmartObjectDefinition,\n): string | undefined {\n if (obj.packageName) return obj.packageName;\n const qualifiedName = obj.qualifiedName || manifestKey;\n const separator = qualifiedName?.lastIndexOf(':') ?? -1;\n return separator > 0 ? qualifiedName?.slice(0, separator) : undefined;\n}\n\ninterface ManifestObjectIndex {\n byExactName: Map<string, SmartObjectDefinition>;\n byPackageAndClass: Map<string, SmartObjectDefinition>;\n bySimpleName: Map<string, SmartObjectDefinition>;\n}\n\nconst manifestObjectIndexes = new WeakMap<\n SmartObjectManifest['objects'],\n ManifestObjectIndex\n>();\n\nfunction packageAndClassKey(packageName: string, className: string): string {\n return `${packageName}\\0${className}`;\n}\n\nfunction getManifestObjectIndex(\n manifest: SmartObjectManifest,\n): ManifestObjectIndex {\n const cached = manifestObjectIndexes.get(manifest.objects);\n if (cached) return cached;\n\n const entries = Object.entries(manifest.objects).sort(\n ([leftKey, left], [rightKey, right]) =>\n compareText(\n left.qualifiedName || leftKey,\n right.qualifiedName || rightKey,\n ) || compareText(leftKey, rightKey),\n );\n const index: ManifestObjectIndex = {\n byExactName: new Map(),\n byPackageAndClass: new Map(),\n bySimpleName: new Map(),\n };\n\n for (const [manifestKey, candidate] of entries) {\n if (!index.byExactName.has(manifestKey)) {\n index.byExactName.set(manifestKey, candidate);\n }\n if (\n candidate.qualifiedName &&\n !index.byExactName.has(candidate.qualifiedName)\n ) {\n index.byExactName.set(candidate.qualifiedName, candidate);\n }\n if (!index.bySimpleName.has(candidate.className)) {\n index.bySimpleName.set(candidate.className, candidate);\n }\n\n const packageName = manifestObjectPackage(manifestKey, candidate);\n if (packageName) {\n const packageKey = packageAndClassKey(packageName, candidate.className);\n if (!index.byPackageAndClass.has(packageKey)) {\n index.byPackageAndClass.set(packageKey, candidate);\n }\n }\n }\n\n manifestObjectIndexes.set(manifest.objects, index);\n return index;\n}\n\n/**\n * Resolve a manifest object deterministically across aggregated packages.\n *\n * Qualified references win exactly. Simple references prefer the referencing\n * object's package, then fall back to a stable qualified/key identity. This is\n * important because aggregated manifests can contain duplicate class names and\n * their object insertion order reflects package discovery order.\n */\nexport function findManifestObjectByName(\n manifest: SmartObjectManifest,\n name: string,\n owner?: SmartObjectDefinition,\n): SmartObjectDefinition | undefined {\n const index = getManifestObjectIndex(manifest);\n const exact = name.includes(':') ? index.byExactName.get(name) : undefined;\n if (exact) return exact;\n\n const ownerPackage = owner\n ? manifestObjectPackage(undefined, owner)\n : undefined;\n const packageLocal = ownerPackage\n ? index.byPackageAndClass.get(packageAndClassKey(ownerPackage, name))\n : undefined;\n\n return packageLocal || index.bySimpleName.get(name);\n}\n\n/**\n * Normalize a relationship field's `related` value to a resolvable class name.\n *\n * Thunk forward-ref decorators — `@foreignKey(() => Scene)`, used heavily in\n * video/voice for models that reference a class declared later — serialize as\n * the RAW arrow-function source string `\"() => Scene\"` in the manifest, which\n * neither `className` nor `qualifiedName` matches. Extract the target class\n * name from the thunk so the edge resolves. Plain (`\"Scene\"`) and qualified\n * (`\"@happyvertical/smrt-assets:Asset\"`) forms contain no `=>` and pass through\n * untouched — the qualified `:` separator is preserved.\n *\n * Kept local to the relationship-edge path on purpose: extends-chain resolution\n * never sees a thunk, so `findManifestObjectByName` and the scanner stay\n * unchanged.\n */\nfunction normalizeRelatedName(related: string): string {\n const thunk = related.match(/=>\\s*([A-Za-z_$][\\w$]*)/);\n return thunk ? thunk[1] : related.trim();\n}\n\n/**\n * True when `obj` is (transitively) a SmrtCollection subclass. Collection\n * classes describe access, not row shapes, so they never become web\n * collection definitions.\n *\n * NOTE: deliberately stronger than sveltekit-generator's private\n * `isCollectionClass`, which only inspects the direct base / a type argument.\n * A deeper subclass (`SpecialWidgetCollection extends WidgetCollection`)\n * carries no type argument of its own; without walking the extends chain it\n * would be mistaken for a model and claim its base model's REST collection.\n */\nexport function isCollectionManifestClass(\n manifest: SmartObjectManifest,\n obj: SmartObjectDefinition,\n seen: Set<string> = new Set(),\n): boolean {\n // Truthy check (not `!== undefined`) mirrors the scanner's own\n // manifest-generator: a scanner that emits `extendsTypeArg: null` for a\n // non-generic base must not be misread as a collection.\n if (obj.extends === 'SmrtCollection' || obj.extendsTypeArg) {\n return true;\n }\n const parentName = obj.extendsQualified || obj.extends;\n if (!parentName || seen.has(parentName)) return false;\n seen.add(parentName);\n const parent = findManifestObjectByName(manifest, parentName, obj);\n return parent ? isCollectionManifestClass(manifest, parent, seen) : false;\n}\n\n/**\n * True when some ancestor model maps to the SAME REST collection (a shared STI\n * table). The STI base model owns the shared table's single definition.\n */\nfunction isStiChildModel(\n manifest: SmartObjectManifest,\n obj: SmartObjectDefinition,\n): boolean {\n const seen = new Set<string>();\n let child = obj;\n let parentName = child.extendsQualified || child.extends;\n while (parentName && !seen.has(parentName)) {\n seen.add(parentName);\n const parent = findManifestObjectByName(manifest, parentName, child);\n if (!parent) return false;\n if (parent.collection === obj.collection) return true;\n child = parent;\n parentName = parent.extendsQualified || parent.extends;\n }\n return false;\n}\n\n/**\n * Select one entry per REST collection whose exposed action set satisfies\n * `qualifies` — STI children folding into their base model, collection classes\n * excluded. Uses the canonical {@link resolveApiActionSet} so the exposed-action\n * set matches exactly what the REST/SvelteKit generators actually emit. Shared\n * by {@link selectWebCollectionEntries} (list-qualified — materializable\n * collections) and {@link selectWebEtagSaltEntries} (get-OR-list — every model\n * with a read route the ETag salt must cover).\n */\nfunction selectEntriesQualifiedBy(\n manifest: SmartObjectManifest,\n qualifies: (actions: ReadonlySet<string>) => boolean,\n): WebCollectionEntry[] {\n const byCollection = new Map<\n string,\n WebCollectionEntry & { isStiChild: boolean }\n >();\n\n for (const obj of Object.values(manifest.objects)) {\n if (isCollectionManifestClass(manifest, obj)) continue;\n\n const exposedActions = resolveApiActionSet(obj);\n if (!qualifies(exposedActions)) continue;\n\n const isStiChild = isStiChildModel(manifest, obj);\n const existing = byCollection.get(obj.collection);\n // One definition per REST collection. The STI BASE model owns it: a child\n // only wins while no base has been recorded yet, so the result is\n // independent of declaration / scan order.\n if (existing && !(existing.isStiChild && !isStiChild)) continue;\n\n byCollection.set(obj.collection, {\n collection: obj.collection,\n obj,\n actions: [...exposedActions].sort(),\n isStiChild,\n });\n }\n\n return [...byCollection.values()].map(({ collection, obj, actions }) => ({\n collection,\n obj,\n actions,\n }));\n}\n\n/**\n * Select the manifest entries that become web collection definitions: one per\n * REST collection, STI children folding into their base model, collection\n * classes excluded, and only models that expose `list` (a read surface is\n * required to MATERIALIZE a client collection — that is what persists).\n */\nexport function selectWebCollectionEntries(\n manifest: SmartObjectManifest,\n): WebCollectionEntry[] {\n return selectEntriesQualifiedBy(manifest, (actions) => actions.has('list'));\n}\n\n/**\n * Select the entries the ETag salt (#1764) must cover: every api-exposed model\n * with a GENERATED READ ROUTE — `list` OR `get`. Broader than\n * {@link selectWebCollectionEntries} on purpose: a get-only model\n * (`api: { include: ['get'] }`) has no materializable client collection (so it\n * never persists), but its generated GET route IS salted, so a shape-only change\n * to it must still change the salt — otherwise a client holding the old concrete\n * ETag would get a zero-query 304 after a shape-only deploy (the #1765 gap the\n * salt closes). Not exported: only {@link computeWebManifestHash} consumes it.\n */\nfunction selectWebEtagSaltEntries(\n manifest: SmartObjectManifest,\n): WebCollectionEntry[] {\n return selectEntriesQualifiedBy(\n manifest,\n (actions) => actions.has('list') || actions.has('get'),\n );\n}\n\n/**\n * Build the per-collection web-collection definition literal — the SINGLE\n * source of truth for the shape emitted by {@link generateWebModule} and hashed\n * by {@link computeWebManifestHash}. Building it in ONE place is a\n * cache-coherency requirement: if the emitted shape and the hashed shape were\n * built independently, adding a field to one and not the other would let the\n * hash silently UNDER-cover a shape change, so persisted caches would not drop\n * and stale rows would hydrate into new code.\n */\nexport function buildWebCollectionDefinition(\n entry: WebCollectionEntry,\n manifest: SmartObjectManifest,\n): {\n name: string;\n className: string;\n endpoint: string;\n idField: string;\n actions: string[];\n fields: Record<string, WebFieldDefinition>;\n relationships: WebRelationship[];\n} {\n return {\n name: entry.collection,\n className: entry.obj.className,\n endpoint: `/${entry.collection}`,\n idField: 'id',\n actions: entry.actions,\n fields: buildWebFieldDefinitions(entry.obj),\n relationships: buildWebRelationships(entry.obj, manifest),\n };\n}\n\n/**\n * Build the informational per-field metadata for a web collection definition:\n * the persisted public-DTO columns only. Relationship pseudo-fields, STI meta\n * internals, transient (unpersisted) and sensitive (wire-stripped) fields are\n * excluded — they are not columns a client reads back over the REST surface.\n */\nexport function buildWebFieldDefinitions(\n obj: SmartObjectDefinition,\n): Record<string, WebFieldDefinition> {\n const fields: Record<string, WebFieldDefinition> = {};\n for (const [fieldName, field] of Object.entries(obj.fields ?? {})) {\n if (RELATIONSHIP_FIELD_TYPES.has(field.type)) continue;\n if (field.type === 'meta') continue;\n if (field.transient) continue;\n if (field.sensitive) continue;\n fields[fieldName] = {\n type: field.type,\n ...(field.required !== undefined ? { required: field.required } : {}),\n ...(field.default !== undefined ? { default: field.default } : {}),\n };\n }\n return fields;\n}\n\n/**\n * Build the manifest-derived relationship edges for a web collection\n * definition (#1761): one entry per relationship field (`foreignKey`,\n * `crossPackageRef`, `oneToMany`, `manyToMany`) whose `related` target resolves\n * to another API-exposed REST collection.\n *\n * These edges drive relationship-derived cache invalidation in the browser\n * client-data runtime: mutating this collection invalidates the caches of the\n * collections named here. The invalidation graph is thus derived entirely from\n * the manifest — no hand-wired cache keys.\n *\n * An edge is SKIPPED (not emitted) when:\n * - `related` is missing, or\n * - `related` cannot be resolved to a manifest object (e.g. a cross-package\n * target not present in this package's manifest), or\n * - the resolved target is not itself an API-exposed web collection (no read\n * surface to invalidate — it never appears in {@link selectWebCollectionEntries}).\n *\n * Self-referential edges (a collection related to itself) are kept: the runtime\n * always invalidates the mutated collection anyway, so a self edge is harmless\n * and keeping it avoids a special case.\n */\nexport function buildWebRelationships(\n obj: SmartObjectDefinition,\n manifest: SmartObjectManifest,\n): WebRelationship[] {\n // The set of REST collections that are actually materialized as web\n // collections. An edge to a model outside this set has no client cache to\n // invalidate, so it is dropped.\n const exposedCollections = new Set(\n selectWebCollectionEntries(manifest).map((entry) => entry.collection),\n );\n\n const relationships: WebRelationship[] = [];\n const seen = new Set<string>();\n for (const [fieldName, field] of Object.entries(obj.fields ?? {})) {\n if (!RELATIONSHIP_EDGE_TYPES.has(field.type)) continue;\n if (!field.related) continue;\n\n // Normalize first: `@foreignKey(() => Scene)` thunks serialize as the raw\n // \"() => Scene\" source, which findManifestObjectByName cannot match on its\n // own.\n const target = findManifestObjectByName(\n manifest,\n normalizeRelatedName(field.related),\n obj,\n );\n if (!target) continue;\n if (!exposedCollections.has(target.collection)) continue;\n\n // De-dupe on (field, relatedCollection): a model never declares the same\n // field twice, but guard anyway so the emitted edge list is stable.\n const dedupeKey = `${fieldName}:${target.collection}`;\n if (seen.has(dedupeKey)) continue;\n seen.add(dedupeKey);\n\n relationships.push({\n field: fieldName,\n kind: field.type as WebRelationshipKind,\n relatedCollection: target.collection,\n });\n }\n return relationships;\n}\n\n/**\n * Build the WebMCP / MCP tool descriptors for a web collection (#1812): one\n * descriptor per exposed action, over the SAME public-DTO fields the definition\n * already exposes (`buildWebFieldDefinitions`). The tool ids match the Node MCP\n * surface (`<class>_<action>`), so a page's WebMCP tools and its MCP-server\n * tools share one vocabulary.\n *\n * Deliberately NOT part of {@link buildWebCollectionDefinition}: descriptors are\n * layered onto the emitted value by {@link generateWebModule} instead, so the\n * #1764 {@link computeWebManifestHash} shape digest keeps hashing ONLY the row\n * shape. That is safe because a descriptor is a pure function of\n * className/actions/fields — all already in the hash — so excluding it never\n * lets the digest under-cover a real shape change.\n */\nexport function buildWebToolDescriptors(\n entry: WebCollectionEntry,\n): ToolDescriptor[] {\n const webFields = buildWebFieldDefinitions(entry.obj);\n const fields: ToolFieldMeta[] = Object.entries(webFields).map(\n ([name, def]) => ({\n name,\n type: def.type,\n ...(def.required !== undefined ? { required: def.required } : {}),\n ...(def.default !== undefined ? { default: def.default } : {}),\n }),\n );\n return buildToolDescriptors({\n className: entry.obj.className,\n fields,\n actions: entry.actions,\n });\n}\n\n/**\n * Recursively sort object keys so structurally-equal values serialize to the\n * SAME JSON regardless of insertion order. Arrays keep their order (order is\n * semantic for `actions`/`relationships`); objects are rebuilt with keys sorted.\n * Required for {@link computeWebManifestHash} to be replica-stable: two builds\n * that produce the same schema but visit the manifest in a different order (map\n * insertion, scan order) must still hash identically, which a plain\n * `JSON.stringify` of insertion-ordered objects would NOT guarantee.\n */\nfunction canonicalize(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map((entry) => canonicalize(entry));\n }\n if (value && typeof value === 'object') {\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n sorted[key] = canonicalize((value as Record<string, unknown>)[key]);\n }\n return sorted;\n }\n return value;\n}\n\n/**\n * A deterministic, replica-stable digest of the web-collection SHAPE (#1764).\n *\n * The hash covers exactly the thing whose change means either old persisted\n * client rows may mis-hydrate OR a stale read ETag would still 304: the same\n * per-collection definition shape {@link generateWebModule} emits — name,\n * className, endpoint, idField, actions, fields, relationships — built via the\n * SHARED {@link buildWebCollectionDefinition} so the hash can never disagree\n * with what is actually shipped. The shape is CANONICALIZED (keys recursively\n * sorted; see {@link canonicalize}) before hashing, so the same schema always\n * yields the same digest across builds and replicas regardless of manifest\n * iteration order.\n *\n * SCOPE — get-OR-list (broader than materializable collections). Covered by\n * {@link selectWebEtagSaltEntries}, so it includes GET-ONLY models too: those do\n * not persist (no materializable collection), but their generated GET route IS\n * salted with this hash, so a shape-only change to a get-only model must change\n * it or a client holding the old concrete ETag gets a zero-query 304 after a\n * shape-only deploy (the #1765 gap the salt closes). The two consumers both use\n * this one value, so it stays identical between them:\n * - `@happyvertical/smrt-web` persistence (#1764) folds it into the durable\n * namespace, so a contract-changing deploy lands on a fresh namespace and old\n * rows are never found (dropped, not mis-hydrated). Including get-only models\n * here is harmless over-invalidation — only list-materializable collections\n * ever hold a persisted snapshot.\n * - the generated read ETag (#1765 salt, #1764) folds it in so a shape-only\n * deploy (no table write) busts every read validator, get-only routes too.\n *\n * Truncated to the first 16 base64url chars: 96 bits is far more than enough to\n * make an accidental shape collision negligible, and a short constant keeps the\n * emitted module and every persistence key compact.\n */\nexport function computeWebManifestHash(manifest: SmartObjectManifest): string {\n const definitions: Record<string, unknown> = {};\n for (const entry of selectWebEtagSaltEntries(manifest)) {\n definitions[entry.collection] = buildWebCollectionDefinition(\n entry,\n manifest,\n );\n }\n const canonicalJson = JSON.stringify(canonicalize(definitions));\n return createHash('sha256')\n .update(canonicalJson)\n .digest('base64url')\n .slice(0, 16);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"web-collections.js","names":[],"sources":["../../src/vite-plugin/web-collections.ts"],"sourcesContent":["/**\n * Manifest → web collection selection (single source of truth).\n *\n * The browser client data runtime (`@happyvertical/smrt-web`, #1761) consumes\n * one typed collection definition per API-exposed REST collection. Three\n * emission sites need the SAME selection and field rules, or the emitted\n * runtime values and their declared types drift apart:\n *\n * - the `\\0smrt:web` runtime virtual module (JSON literal — {@link generateWebModule})\n * - the `@happyvertical/smrt-virt-web` ambient d.ts (vite-plugin)\n * - the physical `@smrt/web` d.ts (prebuild, for `tsc`-only consumers)\n *\n * All three import {@link selectWebCollectionEntries} from here so the value\n * emission and the type emission can never disagree. The per-collection SHAPE\n * (name/className/endpoint/idField/actions/fields/relationships) is built by the\n * ONE {@link buildWebCollectionDefinition}, shared by the runtime emission AND\n * the #1764 {@link computeWebManifestHash} shape digest — so the emitted shape\n * and the hashed shape can never drift (a drift would let the hash under-cover a\n * change → stale client caches).\n */\n\nimport { createHash } from 'node:crypto';\nimport {\n buildToolDescriptors,\n type ToolDescriptor,\n type ToolFieldMeta,\n} from '../generators/tool-schema.js';\nimport type {\n FieldDefinition,\n SmartObjectDefinition,\n SmartObjectManifest,\n} from '../scanner/types.js';\nimport { resolveApiActionSet } from './sveltekit-generator.js';\n\n/**\n * Field types that are relationship pseudo-columns rather than persisted\n * public-DTO columns — they never appear on the wire as scalar values.\n */\nconst RELATIONSHIP_FIELD_TYPES: ReadonlySet<FieldDefinition['type']> = new Set([\n 'oneToMany',\n 'manyToMany',\n]);\n\n/**\n * Field types that describe a relationship to another model. A superset of\n * {@link RELATIONSHIP_FIELD_TYPES}: `foreignKey`/`crossPackageRef` are persisted\n * scalar id columns (they DO appear on the wire), while `oneToMany`/`manyToMany`\n * are pseudo-columns — but all four carry a `related` edge to a sibling model.\n */\nconst RELATIONSHIP_EDGE_TYPES: ReadonlySet<FieldDefinition['type']> = new Set([\n 'foreignKey',\n 'crossPackageRef',\n 'oneToMany',\n 'manyToMany',\n]);\n\n/** Informational per-column metadata carried by a collection definition. */\nexport interface WebFieldDefinition {\n type: FieldDefinition['type'];\n required?: boolean;\n default?: unknown;\n}\n\n/** The relationship kinds a web collection edge can describe. */\nexport type WebRelationshipKind =\n | 'foreignKey'\n | 'crossPackageRef'\n | 'oneToMany'\n | 'manyToMany';\n\n/**\n * One manifest-derived relationship edge from a collection to a sibling REST\n * collection. Consumed by the browser client-data runtime to invalidate\n * dependent collection caches when this collection is mutated (#1761) — the\n * cache-invalidation graph is derived entirely from these edges, never\n * hand-wired. SMRT-owned data: no client-engine type appears here.\n */\nexport interface WebRelationship {\n /** The declaring field carrying the relationship (e.g. `groupId`, `items`). */\n field: string;\n /** The relationship kind, mirroring the manifest field type. */\n kind: WebRelationshipKind;\n /** REST collection name the edge resolves to (e.g. `ad_groups`). */\n relatedCollection: string;\n}\n\n/** One selected REST collection and the model that owns its definition. */\nexport interface WebCollectionEntry {\n /** REST collection name (pluralized), e.g. `products`. */\n collection: string;\n /** The manifest object that owns this collection's definition. */\n obj: SmartObjectDefinition;\n /** Sorted set of exposed CRUD + custom actions. */\n actions: string[];\n}\n\nfunction compareText(left: string, right: string): number {\n if (left < right) return -1;\n if (left > right) return 1;\n return 0;\n}\n\nfunction manifestObjectPackage(\n manifestKey: string | undefined,\n obj: SmartObjectDefinition,\n): string | undefined {\n if (obj.packageName) return obj.packageName;\n const qualifiedName = obj.qualifiedName || manifestKey;\n const separator = qualifiedName?.lastIndexOf(':') ?? -1;\n return separator > 0 ? qualifiedName?.slice(0, separator) : undefined;\n}\n\ninterface ManifestObjectIndex {\n byExactName: Map<string, SmartObjectDefinition>;\n byPackageAndClass: Map<string, SmartObjectDefinition>;\n bySimpleName: Map<string, SmartObjectDefinition>;\n packageByObject: WeakMap<SmartObjectDefinition, string>;\n}\n\nconst manifestObjectIndexes = new WeakMap<\n SmartObjectManifest['objects'],\n ManifestObjectIndex\n>();\n\nfunction packageAndClassKey(packageName: string, className: string): string {\n return `${packageName}\\0${className}`;\n}\n\nfunction getManifestObjectIndex(\n manifest: SmartObjectManifest,\n): ManifestObjectIndex {\n const cached = manifestObjectIndexes.get(manifest.objects);\n if (cached) return cached;\n\n const entries = Object.entries(manifest.objects).sort(\n ([leftKey, left], [rightKey, right]) =>\n compareText(\n left.qualifiedName || leftKey,\n right.qualifiedName || rightKey,\n ) || compareText(leftKey, rightKey),\n );\n const index: ManifestObjectIndex = {\n byExactName: new Map(),\n byPackageAndClass: new Map(),\n bySimpleName: new Map(),\n packageByObject: new WeakMap(),\n };\n\n for (const [manifestKey, candidate] of entries) {\n if (!index.byExactName.has(manifestKey)) {\n index.byExactName.set(manifestKey, candidate);\n }\n if (\n candidate.qualifiedName &&\n !index.byExactName.has(candidate.qualifiedName)\n ) {\n index.byExactName.set(candidate.qualifiedName, candidate);\n }\n if (!index.bySimpleName.has(candidate.className)) {\n index.bySimpleName.set(candidate.className, candidate);\n }\n\n const packageName = manifestObjectPackage(manifestKey, candidate);\n if (packageName) {\n if (!index.packageByObject.has(candidate)) {\n index.packageByObject.set(candidate, packageName);\n }\n const packageKey = packageAndClassKey(packageName, candidate.className);\n if (!index.byPackageAndClass.has(packageKey)) {\n index.byPackageAndClass.set(packageKey, candidate);\n }\n }\n }\n\n manifestObjectIndexes.set(manifest.objects, index);\n return index;\n}\n\nfunction indexedManifestObjectPackage(\n manifest: SmartObjectManifest,\n obj: SmartObjectDefinition,\n): string | undefined {\n const index = getManifestObjectIndex(manifest);\n return (\n index.packageByObject.get(obj) || manifestObjectPackage(undefined, obj)\n );\n}\n\n/**\n * Resolve a manifest object deterministically across aggregated packages.\n *\n * Qualified references win exactly. Simple references prefer the referencing\n * object's package, then fall back to a stable qualified/key identity. This is\n * important because aggregated manifests can contain duplicate class names and\n * their object insertion order reflects package discovery order.\n */\nexport function findManifestObjectByName(\n manifest: SmartObjectManifest,\n name: string,\n owner?: SmartObjectDefinition,\n): SmartObjectDefinition | undefined {\n const index = getManifestObjectIndex(manifest);\n const exact = name.includes(':') ? index.byExactName.get(name) : undefined;\n if (exact) return exact;\n\n const ownerPackage = owner\n ? indexedManifestObjectPackage(manifest, owner)\n : undefined;\n const packageLocal = ownerPackage\n ? index.byPackageAndClass.get(packageAndClassKey(ownerPackage, name))\n : undefined;\n\n return packageLocal || index.bySimpleName.get(name);\n}\n\n/**\n * Normalize a relationship field's `related` value to a resolvable class name.\n *\n * Thunk forward-ref decorators — `@foreignKey(() => Scene)`, used heavily in\n * video/voice for models that reference a class declared later — serialize as\n * the RAW arrow-function source string `\"() => Scene\"` in the manifest, which\n * neither `className` nor `qualifiedName` matches. Extract the target class\n * name from the thunk so the edge resolves. Plain (`\"Scene\"`) and qualified\n * (`\"@happyvertical/smrt-assets:Asset\"`) forms contain no `=>` and pass through\n * untouched — the qualified `:` separator is preserved.\n *\n * Kept local to the relationship-edge path on purpose: extends-chain resolution\n * never sees a thunk, so `findManifestObjectByName` and the scanner stay\n * unchanged.\n */\nfunction normalizeRelatedName(related: string): string {\n const thunk = related.match(/=>\\s*([A-Za-z_$][\\w$]*)/);\n return thunk ? thunk[1] : related.trim();\n}\n\n/**\n * True when `obj` is (transitively) a SmrtCollection subclass. Collection\n * classes describe access, not row shapes, so they never become web\n * collection definitions.\n *\n * NOTE: deliberately stronger than sveltekit-generator's private\n * `isCollectionClass`, which only inspects the direct base / a type argument.\n * A deeper subclass (`SpecialWidgetCollection extends WidgetCollection`)\n * carries no type argument of its own; without walking the extends chain it\n * would be mistaken for a model and claim its base model's REST collection.\n */\nexport function isCollectionManifestClass(\n manifest: SmartObjectManifest,\n obj: SmartObjectDefinition,\n seen: Set<string> = new Set(),\n): boolean {\n // Truthy check (not `!== undefined`) mirrors the scanner's own\n // manifest-generator: a scanner that emits `extendsTypeArg: null` for a\n // non-generic base must not be misread as a collection.\n if (obj.extends === 'SmrtCollection' || obj.extendsTypeArg) {\n return true;\n }\n const parentName = obj.extendsQualified || obj.extends;\n if (!parentName || seen.has(parentName)) return false;\n seen.add(parentName);\n const parent = findManifestObjectByName(manifest, parentName, obj);\n return parent ? isCollectionManifestClass(manifest, parent, seen) : false;\n}\n\n/**\n * Resolve the row model carried by a collection class.\n *\n * An explicit generic on any ancestor is authoritative. Conventional\n * `FooCollection` → `Foo` lookup is only a fallback after the full ancestry\n * has been checked, so an unrelated `SpecialFoo` cannot override an inherited\n * `FooCollection<Foo>` contract.\n */\nfunction collectionAncestry(\n manifest: SmartObjectManifest,\n obj: SmartObjectDefinition,\n): SmartObjectDefinition[] {\n const ancestry: SmartObjectDefinition[] = [];\n const seen = new Set<string>();\n let candidate: SmartObjectDefinition | undefined = obj;\n\n while (candidate) {\n ancestry.push(candidate);\n const parentName = candidate.extendsQualified || candidate.extends;\n if (!parentName || seen.has(parentName)) break;\n seen.add(parentName);\n candidate = findManifestObjectByName(manifest, parentName, candidate);\n }\n return ancestry;\n}\n\n/**\n * Resolve the authoritative collection item type name even when a partial\n * manifest omits the item definition itself.\n */\ninterface CollectionItemReference {\n name: string;\n owner: SmartObjectDefinition;\n object?: SmartObjectDefinition;\n}\n\nfunction resolveCollectionItemCandidate(\n manifest: SmartObjectManifest,\n name: string,\n owner: SmartObjectDefinition,\n): CollectionItemReference {\n const object = findManifestObjectByName(manifest, name, owner);\n const ownerPackage = indexedManifestObjectPackage(manifest, owner);\n\n if (!name.includes(':') && ownerPackage) {\n const objectPackage = object\n ? indexedManifestObjectPackage(manifest, object)\n : undefined;\n if (objectPackage !== ownerPackage) {\n return {\n name: `${ownerPackage}:${name}`,\n owner,\n };\n }\n }\n\n return { name, owner, object };\n}\n\nfunction resolveCollectionItemReference(\n manifest: SmartObjectManifest,\n obj: SmartObjectDefinition,\n): CollectionItemReference | undefined {\n const ancestry = collectionAncestry(manifest, obj);\n for (const collectionClass of ancestry) {\n if (collectionClass.extendsTypeArg) {\n return resolveCollectionItemCandidate(\n manifest,\n collectionClass.extendsTypeArg,\n collectionClass,\n );\n }\n }\n\n let fallback: CollectionItemReference | undefined;\n for (const collectionClass of ancestry) {\n if (!collectionClass.className.endsWith('Collection')) continue;\n const conventionalName = collectionClass.className.slice(\n 0,\n -'Collection'.length,\n );\n const candidate = resolveCollectionItemCandidate(\n manifest,\n conventionalName,\n collectionClass,\n );\n if (candidate.object) return candidate;\n fallback ??= candidate;\n }\n\n return fallback;\n}\n\nexport function resolveCollectionItemTypeName(\n manifest: SmartObjectManifest,\n obj: SmartObjectDefinition,\n): string | undefined {\n return resolveCollectionItemReference(manifest, obj)?.name;\n}\n\nexport function resolveCollectionItemObject(\n manifest: SmartObjectManifest,\n obj: SmartObjectDefinition,\n): SmartObjectDefinition | undefined {\n return resolveCollectionItemReference(manifest, obj)?.object;\n}\n\n/**\n * True when some ancestor model maps to the SAME REST collection (a shared STI\n * table). The STI base model owns the shared table's single definition.\n */\nfunction isStiChildModel(\n manifest: SmartObjectManifest,\n obj: SmartObjectDefinition,\n): boolean {\n const seen = new Set<string>();\n let child = obj;\n let parentName = child.extendsQualified || child.extends;\n while (parentName && !seen.has(parentName)) {\n seen.add(parentName);\n const parent = findManifestObjectByName(manifest, parentName, child);\n if (!parent) return false;\n if (parent.collection === obj.collection) return true;\n child = parent;\n parentName = parent.extendsQualified || parent.extends;\n }\n return false;\n}\n\n/**\n * Select one entry per REST collection whose exposed action set satisfies\n * `qualifies` — STI children folding into their base model, collection classes\n * excluded. Uses the canonical {@link resolveApiActionSet} so the exposed-action\n * set matches exactly what the REST/SvelteKit generators actually emit. Shared\n * by {@link selectWebCollectionEntries} (list-qualified — materializable\n * collections) and {@link selectWebEtagSaltEntries} (get-OR-list — every model\n * with a read route the ETag salt must cover).\n */\nfunction selectEntriesQualifiedBy(\n manifest: SmartObjectManifest,\n qualifies: (actions: ReadonlySet<string>) => boolean,\n): WebCollectionEntry[] {\n const byCollection = new Map<\n string,\n WebCollectionEntry & { isStiChild: boolean }\n >();\n\n for (const obj of Object.values(manifest.objects)) {\n if (isCollectionManifestClass(manifest, obj)) continue;\n\n const exposedActions = resolveApiActionSet(obj);\n if (!qualifies(exposedActions)) continue;\n\n const isStiChild = isStiChildModel(manifest, obj);\n const existing = byCollection.get(obj.collection);\n // One definition per REST collection. The STI BASE model owns it: a child\n // only wins while no base has been recorded yet, so the result is\n // independent of declaration / scan order.\n if (existing && !(existing.isStiChild && !isStiChild)) continue;\n\n byCollection.set(obj.collection, {\n collection: obj.collection,\n obj,\n actions: [...exposedActions].sort(),\n isStiChild,\n });\n }\n\n return [...byCollection.values()].map(({ collection, obj, actions }) => ({\n collection,\n obj,\n actions,\n }));\n}\n\n/**\n * Select the manifest entries that become web collection definitions: one per\n * REST collection, STI children folding into their base model, collection\n * classes excluded, and only models that expose `list` (a read surface is\n * required to MATERIALIZE a client collection — that is what persists).\n */\nexport function selectWebCollectionEntries(\n manifest: SmartObjectManifest,\n): WebCollectionEntry[] {\n return selectEntriesQualifiedBy(manifest, (actions) => actions.has('list'));\n}\n\n/**\n * Select the entries the ETag salt (#1764) must cover: every api-exposed model\n * with a GENERATED READ ROUTE — `list` OR `get`. Broader than\n * {@link selectWebCollectionEntries} on purpose: a get-only model\n * (`api: { include: ['get'] }`) has no materializable client collection (so it\n * never persists), but its generated GET route IS salted, so a shape-only change\n * to it must still change the salt — otherwise a client holding the old concrete\n * ETag would get a zero-query 304 after a shape-only deploy (the #1765 gap the\n * salt closes). Not exported: only {@link computeWebManifestHash} consumes it.\n */\nfunction selectWebEtagSaltEntries(\n manifest: SmartObjectManifest,\n): WebCollectionEntry[] {\n return selectEntriesQualifiedBy(\n manifest,\n (actions) => actions.has('list') || actions.has('get'),\n );\n}\n\n/**\n * Build the per-collection web-collection definition literal — the SINGLE\n * source of truth for the shape emitted by {@link generateWebModule} and hashed\n * by {@link computeWebManifestHash}. Building it in ONE place is a\n * cache-coherency requirement: if the emitted shape and the hashed shape were\n * built independently, adding a field to one and not the other would let the\n * hash silently UNDER-cover a shape change, so persisted caches would not drop\n * and stale rows would hydrate into new code.\n */\nexport function buildWebCollectionDefinition(\n entry: WebCollectionEntry,\n manifest: SmartObjectManifest,\n): {\n name: string;\n className: string;\n endpoint: string;\n idField: string;\n actions: string[];\n fields: Record<string, WebFieldDefinition>;\n relationships: WebRelationship[];\n} {\n return {\n name: entry.collection,\n className: entry.obj.className,\n endpoint: `/${entry.collection}`,\n idField: 'id',\n actions: entry.actions,\n fields: buildWebFieldDefinitions(entry.obj),\n relationships: buildWebRelationships(entry.obj, manifest),\n };\n}\n\n/**\n * Build the informational per-field metadata for a web collection definition:\n * the persisted public-DTO columns only. Relationship pseudo-fields, STI meta\n * internals, transient (unpersisted) and sensitive (wire-stripped) fields are\n * excluded — they are not columns a client reads back over the REST surface.\n */\nexport function buildWebFieldDefinitions(\n obj: SmartObjectDefinition,\n): Record<string, WebFieldDefinition> {\n const fields: Record<string, WebFieldDefinition> = {};\n for (const [fieldName, field] of Object.entries(obj.fields ?? {})) {\n if (RELATIONSHIP_FIELD_TYPES.has(field.type)) continue;\n if (field.type === 'meta') continue;\n if (field.transient) continue;\n if (field.sensitive) continue;\n fields[fieldName] = {\n type: field.type,\n ...(field.required !== undefined ? { required: field.required } : {}),\n ...(field.default !== undefined ? { default: field.default } : {}),\n };\n }\n return fields;\n}\n\n/**\n * Build the manifest-derived relationship edges for a web collection\n * definition (#1761): one entry per relationship field (`foreignKey`,\n * `crossPackageRef`, `oneToMany`, `manyToMany`) whose `related` target resolves\n * to another API-exposed REST collection.\n *\n * These edges drive relationship-derived cache invalidation in the browser\n * client-data runtime: mutating this collection invalidates the caches of the\n * collections named here. The invalidation graph is thus derived entirely from\n * the manifest — no hand-wired cache keys.\n *\n * An edge is SKIPPED (not emitted) when:\n * - `related` is missing, or\n * - `related` cannot be resolved to a manifest object (e.g. a cross-package\n * target not present in this package's manifest), or\n * - the resolved target is not itself an API-exposed web collection (no read\n * surface to invalidate — it never appears in {@link selectWebCollectionEntries}).\n *\n * Self-referential edges (a collection related to itself) are kept: the runtime\n * always invalidates the mutated collection anyway, so a self edge is harmless\n * and keeping it avoids a special case.\n */\nexport function buildWebRelationships(\n obj: SmartObjectDefinition,\n manifest: SmartObjectManifest,\n): WebRelationship[] {\n // The set of REST collections that are actually materialized as web\n // collections. An edge to a model outside this set has no client cache to\n // invalidate, so it is dropped.\n const exposedCollections = new Set(\n selectWebCollectionEntries(manifest).map((entry) => entry.collection),\n );\n\n const relationships: WebRelationship[] = [];\n const seen = new Set<string>();\n for (const [fieldName, field] of Object.entries(obj.fields ?? {})) {\n if (!RELATIONSHIP_EDGE_TYPES.has(field.type)) continue;\n if (!field.related) continue;\n\n // Normalize first: `@foreignKey(() => Scene)` thunks serialize as the raw\n // \"() => Scene\" source, which findManifestObjectByName cannot match on its\n // own.\n const target = findManifestObjectByName(\n manifest,\n normalizeRelatedName(field.related),\n obj,\n );\n if (!target) continue;\n if (!exposedCollections.has(target.collection)) continue;\n\n // De-dupe on (field, relatedCollection): a model never declares the same\n // field twice, but guard anyway so the emitted edge list is stable.\n const dedupeKey = `${fieldName}:${target.collection}`;\n if (seen.has(dedupeKey)) continue;\n seen.add(dedupeKey);\n\n relationships.push({\n field: fieldName,\n kind: field.type as WebRelationshipKind,\n relatedCollection: target.collection,\n });\n }\n return relationships;\n}\n\n/**\n * Build the WebMCP / MCP tool descriptors for a web collection (#1812): one\n * descriptor per exposed action, over the SAME public-DTO fields the definition\n * already exposes (`buildWebFieldDefinitions`). The tool ids match the Node MCP\n * surface (`<class>_<action>`), so a page's WebMCP tools and its MCP-server\n * tools share one vocabulary.\n *\n * Deliberately NOT part of {@link buildWebCollectionDefinition}: descriptors are\n * layered onto the emitted value by {@link generateWebModule} instead, so the\n * #1764 {@link computeWebManifestHash} shape digest keeps hashing ONLY the row\n * shape. That is safe because a descriptor is a pure function of\n * className/actions/fields — all already in the hash — so excluding it never\n * lets the digest under-cover a real shape change.\n */\nexport function buildWebToolDescriptors(\n entry: WebCollectionEntry,\n): ToolDescriptor[] {\n const webFields = buildWebFieldDefinitions(entry.obj);\n const fields: ToolFieldMeta[] = Object.entries(webFields).map(\n ([name, def]) => ({\n name,\n type: def.type,\n ...(def.required !== undefined ? { required: def.required } : {}),\n ...(def.default !== undefined ? { default: def.default } : {}),\n }),\n );\n return buildToolDescriptors({\n className: entry.obj.className,\n fields,\n actions: entry.actions,\n });\n}\n\n/**\n * Recursively sort object keys so structurally-equal values serialize to the\n * SAME JSON regardless of insertion order. Arrays keep their order (order is\n * semantic for `actions`/`relationships`); objects are rebuilt with keys sorted.\n * Required for {@link computeWebManifestHash} to be replica-stable: two builds\n * that produce the same schema but visit the manifest in a different order (map\n * insertion, scan order) must still hash identically, which a plain\n * `JSON.stringify` of insertion-ordered objects would NOT guarantee.\n */\nfunction canonicalize(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map((entry) => canonicalize(entry));\n }\n if (value && typeof value === 'object') {\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n sorted[key] = canonicalize((value as Record<string, unknown>)[key]);\n }\n return sorted;\n }\n return value;\n}\n\n/**\n * A deterministic, replica-stable digest of the web-collection SHAPE (#1764).\n *\n * The hash covers exactly the thing whose change means either old persisted\n * client rows may mis-hydrate OR a stale read ETag would still 304: the same\n * per-collection definition shape {@link generateWebModule} emits — name,\n * className, endpoint, idField, actions, fields, relationships — built via the\n * SHARED {@link buildWebCollectionDefinition} so the hash can never disagree\n * with what is actually shipped. The shape is CANONICALIZED (keys recursively\n * sorted; see {@link canonicalize}) before hashing, so the same schema always\n * yields the same digest across builds and replicas regardless of manifest\n * iteration order.\n *\n * SCOPE — get-OR-list (broader than materializable collections). Covered by\n * {@link selectWebEtagSaltEntries}, so it includes GET-ONLY models too: those do\n * not persist (no materializable collection), but their generated GET route IS\n * salted with this hash, so a shape-only change to a get-only model must change\n * it or a client holding the old concrete ETag gets a zero-query 304 after a\n * shape-only deploy (the #1765 gap the salt closes). The two consumers both use\n * this one value, so it stays identical between them:\n * - `@happyvertical/smrt-web` persistence (#1764) folds it into the durable\n * namespace, so a contract-changing deploy lands on a fresh namespace and old\n * rows are never found (dropped, not mis-hydrated). Including get-only models\n * here is harmless over-invalidation — only list-materializable collections\n * ever hold a persisted snapshot.\n * - the generated read ETag (#1765 salt, #1764) folds it in so a shape-only\n * deploy (no table write) busts every read validator, get-only routes too.\n *\n * Truncated to the first 16 base64url chars: 96 bits is far more than enough to\n * make an accidental shape collision negligible, and a short constant keeps the\n * emitted module and every persistence key compact.\n */\nexport function computeWebManifestHash(manifest: SmartObjectManifest): string {\n const definitions: Record<string, unknown> = {};\n for (const entry of selectWebEtagSaltEntries(manifest)) {\n definitions[entry.collection] = buildWebCollectionDefinition(\n entry,\n manifest,\n );\n }\n const canonicalJson = JSON.stringify(canonicalize(definitions));\n return createHash('sha256')\n .update(canonicalJson)\n .digest('base64url')\n .slice(0, 16);\n}\n"],"mappings":"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"}
|
package/dist/vite-plugin.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { findCliApiCoherenceViolations, generateSvelteKitRoutes, methodNameToKebab, resolveApiActionSet, validateCliIncludeAgainstApi } from "./vite-plugin/sveltekit-generator.js";
|
|
2
|
-
import { generateInlineRegisterModule, isRegisterShimModuleId, smrtPlugin } from "./vite-plugin/index.js";
|
|
3
|
-
export { findCliApiCoherenceViolations, generateInlineRegisterModule, generateSvelteKitRoutes, isRegisterShimModuleId, methodNameToKebab, resolveApiActionSet, smrtPlugin, validateCliIncludeAgainstApi };
|
|
2
|
+
import { generateInlineRegisterModule, generateTypeDeclarationFile, isRegisterShimModuleId, smrtPlugin } from "./vite-plugin/index.js";
|
|
3
|
+
export { findCliApiCoherenceViolations, generateInlineRegisterModule, generateSvelteKitRoutes, generateTypeDeclarationFile, isRegisterShimModuleId, methodNameToKebab, resolveApiActionSet, smrtPlugin, validateCliIncludeAgainstApi };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-core",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.32",
|
|
4
4
|
"description": "Core AI agent framework with standardized collections, object-relational mapping, and code generators",
|
|
5
5
|
"author": "HappyVertical",
|
|
6
6
|
"type": "module",
|
|
@@ -165,9 +165,9 @@
|
|
|
165
165
|
"tsx": "^4.23.0",
|
|
166
166
|
"typescript": "5.9.3",
|
|
167
167
|
"yaml": "^2.9.0",
|
|
168
|
-
"@happyvertical/smrt-config": "0.40.
|
|
169
|
-
"@happyvertical/smrt-scanner": "0.40.
|
|
170
|
-
"@happyvertical/smrt-types": "0.40.
|
|
168
|
+
"@happyvertical/smrt-config": "0.40.32",
|
|
169
|
+
"@happyvertical/smrt-scanner": "0.40.32",
|
|
170
|
+
"@happyvertical/smrt-types": "0.40.32"
|
|
171
171
|
},
|
|
172
172
|
"peerDependencies": {
|
|
173
173
|
"@huggingface/transformers": ">=3.0.0 <4.0.0",
|