@happyvertical/smrt-core 0.53.4 → 0.53.6
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/AGENTS.md +4 -0
- package/README.md +11 -0
- package/agents/build-knowledge.md +7 -0
- package/agents/generators.md +24 -1
- package/dist/generated-collection-access.d.ts +43 -0
- package/dist/generated-collection-access.d.ts.map +1 -0
- package/dist/generated-collection-access.js +71 -0
- package/dist/generated-collection-access.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +3 -1
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/smrt-knowledge.json +9 -9
- package/dist/utils/qualified-names.d.ts +15 -0
- package/dist/utils/qualified-names.d.ts.map +1 -1
- package/dist/utils/qualified-names.js +31 -1
- package/dist/utils/qualified-names.js.map +1 -1
- package/dist/vite-plugin/changes-route.d.ts +4 -3
- package/dist/vite-plugin/changes-route.d.ts.map +1 -1
- package/dist/vite-plugin/changes-route.js +8 -7
- package/dist/vite-plugin/changes-route.js.map +1 -1
- package/dist/vite-plugin/dev-plane-route.d.ts.map +1 -1
- package/dist/vite-plugin/dev-plane-route.js +4 -4
- package/dist/vite-plugin/dev-plane-route.js.map +1 -1
- package/dist/vite-plugin/events-route.d.ts.map +1 -1
- package/dist/vite-plugin/events-route.js +4 -4
- package/dist/vite-plugin/events-route.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +13 -1
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/resources-route.d.ts.map +1 -1
- package/dist/vite-plugin/resources-route.js +9 -4
- package/dist/vite-plugin/resources-route.js.map +1 -1
- package/dist/vite-plugin/sveltekit-config-import.d.ts +22 -0
- package/dist/vite-plugin/sveltekit-config-import.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-config-import.js +44 -2
- package/dist/vite-plugin/sveltekit-config-import.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +59 -9
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/sveltekit-register-injection.d.ts +26 -0
- package/dist/vite-plugin/sveltekit-register-injection.d.ts.map +1 -0
- package/dist/vite-plugin/sveltekit-register-injection.js +37 -0
- package/dist/vite-plugin/sveltekit-register-injection.js.map +1 -0
- package/dist/vite-plugin/sync-apply-route.d.ts +1 -1
- package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
- package/dist/vite-plugin/sync-apply-route.js +4 -4
- package/dist/vite-plugin/sync-apply-route.js.map +1 -1
- package/package.json +10 -10
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register generated objects before the application's server config module
|
|
3
|
+
* runs (#3416).
|
|
4
|
+
*
|
|
5
|
+
* The config module (`src/lib/server/smrt.ts` by default) creates the app
|
|
6
|
+
* runtime and is the first SMRT module the server imports (`hooks.server.ts`,
|
|
7
|
+
* page loads, and every generated route's access prelude import it). The
|
|
8
|
+
* plugin generates `smrt-register.ts` beside it on every dev/build run, so it
|
|
9
|
+
* prepends that import to the config module instead of each application
|
|
10
|
+
* hand-writing it behind a "not generated yet" guard. Registration stays the
|
|
11
|
+
* generated module's deterministic `consumer.packages` + local-object order.
|
|
12
|
+
*/
|
|
13
|
+
/** File name of the generated registration module beside the config module. */
|
|
14
|
+
export declare const SVELTEKIT_REGISTRATION_FILE_NAME = "smrt-register.ts";
|
|
15
|
+
export interface SvelteKitRegisterInjectionOptions {
|
|
16
|
+
projectRoot: string;
|
|
17
|
+
configPath?: string;
|
|
18
|
+
configFileName?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Return `code` with the generated registration import prepended when `id` is
|
|
22
|
+
* the configured config module and the registration module exists; otherwise
|
|
23
|
+
* `null`. The import shares the first line, so later line numbers are kept.
|
|
24
|
+
*/
|
|
25
|
+
export declare function injectSvelteKitRegistration(code: string, id: string, options: SvelteKitRegisterInjectionOptions): string | null;
|
|
26
|
+
//# sourceMappingURL=sveltekit-register-injection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sveltekit-register-injection.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/sveltekit-register-injection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,+EAA+E;AAC/E,eAAO,MAAM,gCAAgC,qBAAqB,CAAC;AAInE,MAAM,WAAW,iCAAiC;IAChD,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAMD;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,iCAAiC,GACzC,MAAM,GAAG,IAAI,CAWf"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
//#region src/vite-plugin/sveltekit-register-injection.ts
|
|
4
|
+
/**
|
|
5
|
+
* Register generated objects before the application's server config module
|
|
6
|
+
* runs (#3416).
|
|
7
|
+
*
|
|
8
|
+
* The config module (`src/lib/server/smrt.ts` by default) creates the app
|
|
9
|
+
* runtime and is the first SMRT module the server imports (`hooks.server.ts`,
|
|
10
|
+
* page loads, and every generated route's access prelude import it). The
|
|
11
|
+
* plugin generates `smrt-register.ts` beside it on every dev/build run, so it
|
|
12
|
+
* prepends that import to the config module instead of each application
|
|
13
|
+
* hand-writing it behind a "not generated yet" guard. Registration stays the
|
|
14
|
+
* generated module's deterministic `consumer.packages` + local-object order.
|
|
15
|
+
*/
|
|
16
|
+
/** File name of the generated registration module beside the config module. */
|
|
17
|
+
var SVELTEKIT_REGISTRATION_FILE_NAME = "smrt-register.ts";
|
|
18
|
+
var REGISTRATION_IMPORT = "import './smrt-register.js';";
|
|
19
|
+
function normalizeModulePath(id) {
|
|
20
|
+
return resolve(id.replace(/[?#].*$/, "")).replace(/\\/g, "/");
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Return `code` with the generated registration import prepended when `id` is
|
|
24
|
+
* the configured config module and the registration module exists; otherwise
|
|
25
|
+
* `null`. The import shares the first line, so later line numbers are kept.
|
|
26
|
+
*/
|
|
27
|
+
function injectSvelteKitRegistration(code, id, options) {
|
|
28
|
+
const configDir = resolve(options.projectRoot, options.configPath || "src/lib/server");
|
|
29
|
+
const configFile = join(configDir, options.configFileName || "smrt.ts");
|
|
30
|
+
if (normalizeModulePath(id) !== normalizeModulePath(configFile)) return null;
|
|
31
|
+
if (!existsSync(join(configDir, "smrt-register.ts"))) return null;
|
|
32
|
+
return `${REGISTRATION_IMPORT}${code}`;
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { SVELTEKIT_REGISTRATION_FILE_NAME, injectSvelteKitRegistration };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=sveltekit-register-injection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sveltekit-register-injection.js","names":[],"sources":["../../src/vite-plugin/sveltekit-register-injection.ts"],"sourcesContent":["/**\n * Register generated objects before the application's server config module\n * runs (#3416).\n *\n * The config module (`src/lib/server/smrt.ts` by default) creates the app\n * runtime and is the first SMRT module the server imports (`hooks.server.ts`,\n * page loads, and every generated route's access prelude import it). The\n * plugin generates `smrt-register.ts` beside it on every dev/build run, so it\n * prepends that import to the config module instead of each application\n * hand-writing it behind a \"not generated yet\" guard. Registration stays the\n * generated module's deterministic `consumer.packages` + local-object order.\n */\n\nimport { existsSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\n\n/** File name of the generated registration module beside the config module. */\nexport const SVELTEKIT_REGISTRATION_FILE_NAME = 'smrt-register.ts';\n\nconst REGISTRATION_IMPORT = \"import './smrt-register.js';\";\n\nexport interface SvelteKitRegisterInjectionOptions {\n projectRoot: string;\n configPath?: string;\n configFileName?: string;\n}\n\nfunction normalizeModulePath(id: string): string {\n return resolve(id.replace(/[?#].*$/, '')).replace(/\\\\/g, '/');\n}\n\n/**\n * Return `code` with the generated registration import prepended when `id` is\n * the configured config module and the registration module exists; otherwise\n * `null`. The import shares the first line, so later line numbers are kept.\n */\nexport function injectSvelteKitRegistration(\n code: string,\n id: string,\n options: SvelteKitRegisterInjectionOptions,\n): string | null {\n const configDir = resolve(\n options.projectRoot,\n options.configPath || 'src/lib/server',\n );\n const configFile = join(configDir, options.configFileName || 'smrt.ts');\n if (normalizeModulePath(id) !== normalizeModulePath(configFile)) return null;\n if (!existsSync(join(configDir, SVELTEKIT_REGISTRATION_FILE_NAME))) {\n return null;\n }\n return `${REGISTRATION_IMPORT}${code}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,IAAa,mCAAmC;AAEhD,IAAM,sBAAsB;AAQ5B,SAAS,oBAAoB,IAAoB;CAC/C,OAAO,QAAQ,GAAG,QAAQ,WAAW,EAAE,CAAC,CAAC,CAAC,QAAQ,OAAO,GAAG;AAC9D;;;;;;AAOA,SAAgB,4BACd,MACA,IACA,SACe;CACf,MAAM,YAAY,QAChB,QAAQ,aACR,QAAQ,cAAc,gBACxB;CACA,MAAM,aAAa,KAAK,WAAW,QAAQ,kBAAkB,SAAS;CACtE,IAAI,oBAAoB,EAAE,MAAM,oBAAoB,UAAU,GAAG,OAAO;CACxE,IAAI,CAAC,WAAW,KAAK,WAAA,kBAA2C,CAAC,GAC/D,OAAO;CAET,OAAO,GAAG,sBAAsB;AAClC"}
|
|
@@ -52,7 +52,7 @@ export declare function collectSyncApplyTargets(manifest: SmartObjectManifest):
|
|
|
52
52
|
* Render the generated `+server.ts` content for the sync-apply route.
|
|
53
53
|
* Exported for tests.
|
|
54
54
|
*/
|
|
55
|
-
export declare function generateSyncApplyRouteTemplate(targets: SyncTargetSpec[],
|
|
55
|
+
export declare function generateSyncApplyRouteTemplate(targets: SyncTargetSpec[], collectionAccessImports: string): string;
|
|
56
56
|
/**
|
|
57
57
|
* Generate `{routesDir}/sync/apply/+server.ts` for the manifest's syncable
|
|
58
58
|
* models. Returns true when a route was written. Skips generation (with a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-apply-route.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/sync-apply-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAEV,mBAAmB,EACpB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAGjE,QAAA,MAAM,gBAAgB,yCAA0C,CAAC;AAEjE,KAAK,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAExD,wDAAwD;AACxD,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,oBAAoB,EAAE,MAAM,CAAC;IAC7B,GAAG,EAAE,cAAc,EAAE,CAAC;IACtB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,iBAAiB,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACnC,YAAY,EAAE,OAAO,GAAG,MAAM,CAAC;IAC/B,YAAY,EAAE,OAAO,CAAC;CACvB;AA6ED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,mBAAmB,GAC5B,cAAc,EAAE,CAiClB;AAiBD;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,cAAc,EAAE,EACzB,
|
|
1
|
+
{"version":3,"file":"sync-apply-route.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/sync-apply-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAEV,mBAAmB,EACpB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAGjE,QAAA,MAAM,gBAAgB,yCAA0C,CAAC;AAEjE,KAAK,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAExD,wDAAwD;AACxD,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,oBAAoB,EAAE,MAAM,CAAC;IAC7B,GAAG,EAAE,cAAc,EAAE,CAAC;IACtB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,iBAAiB,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACnC,YAAY,EAAE,OAAO,GAAG,MAAM,CAAC;IAC/B,YAAY,EAAE,OAAO,CAAC;CACvB;AA6ED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,mBAAmB,GAC5B,cAAc,EAAE,CAiClB;AAiBD;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,cAAc,EAAE,EACzB,uBAAuB,EAAE,MAAM,GAC9B,MAAM,CAkJR;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAmCT"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isFrameworkBaseClass } from "../registry/framework-base-classes.js";
|
|
2
2
|
import { AUTO_GENERATED_ROUTE_HEADER } from "./route-header.js";
|
|
3
|
-
import {
|
|
3
|
+
import { generateCollectionAccessImports } from "./sveltekit-config-import.js";
|
|
4
4
|
import { isCollectionManifestClass } from "./web-collections.js";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
@@ -117,7 +117,7 @@ function serializeTargets(targets) {
|
|
|
117
117
|
* Render the generated `+server.ts` content for the sync-apply route.
|
|
118
118
|
* Exported for tests.
|
|
119
119
|
*/
|
|
120
|
-
function generateSyncApplyRouteTemplate(targets,
|
|
120
|
+
function generateSyncApplyRouteTemplate(targets, collectionAccessImports) {
|
|
121
121
|
const anyTenantScoped = targets.some((target) => target.tenantScoped);
|
|
122
122
|
return `${AUTO_GENERATED_ROUTE_HEADER}
|
|
123
123
|
// DO NOT EDIT - changes will be overwritten
|
|
@@ -133,8 +133,8 @@ import {
|
|
|
133
133
|
type SyncApplyTarget,
|
|
134
134
|
} from '@happyvertical/smrt-core';
|
|
135
135
|
import { json } from '@sveltejs/kit';
|
|
136
|
-
import { getCollection } from '${configImport}';
|
|
137
136
|
import type { RequestHandler } from './$types';
|
|
137
|
+
${collectionAccessImports}
|
|
138
138
|
${anyTenantScoped ? `
|
|
139
139
|
import { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenancy';
|
|
140
140
|
|
|
@@ -277,7 +277,7 @@ function generateSyncApplyRoute(projectRoot, manifest, options) {
|
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
279
|
if (!existsSync(routeDir)) mkdirSync(routeDir, { recursive: true });
|
|
280
|
-
writeFileSync(routePath, generateSyncApplyRouteTemplate(targets,
|
|
280
|
+
writeFileSync(routePath, generateSyncApplyRouteTemplate(targets, generateCollectionAccessImports(projectRoot, routeDir, options, ["getCollection"])), "utf-8");
|
|
281
281
|
console.log(`[smrt] Generated: ${routePath}`);
|
|
282
282
|
return true;
|
|
283
283
|
}
|
|
@@ -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 { isFrameworkBaseClass } from '../registry/framework-base-classes.js';\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 { resolveSvelteKitConfigImport } from './sveltekit-config-import.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 /**\n * `<collection>` prefix of the `<collection>.<action>` operation permission\n * the mirrored generated write routes require (#2977) — the same\n * `decoratorConfig.collection ?? collection` resolution as\n * sveltekit-generator.ts `resolvePermissionCollection`.\n */\n permissionCollection: 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 `resolvePermissionCollection` (#2977). */\nfunction resolvePermissionCollection(objectDef: SmartObjectDefinition): string {\n const configured = (\n objectDef.decoratorConfig as { collection?: unknown } | undefined\n )?.collection;\n return typeof configured === 'string' && configured.length > 0\n ? configured\n : objectDef.collection;\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 // The framework's own abstract base classes are scaffolding, not\n // resources — never make them a sync-apply target, regardless of\n // config (#2642).\n if (isFrameworkBaseClass(objectDef.className, objectDef.packageName)) {\n continue;\n }\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 permissionCollection: resolvePermissionCollection(objectDef),\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 `permissionCollection: ${JSON.stringify(target.permissionCollection)}`,\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 configImport: string,\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 '${configImport}';\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 /** \\`<collection>\\` prefix of the required \\`<collection>.<op>\\` permission. */\n permissionCollection: 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\nfunction readRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object'\n ? (value as Record<string, unknown>)\n : {};\n}\n\nfunction hasGrantedPermission(value: unknown, permission: string): boolean {\n if (Array.isArray(value)) return value.includes(permission);\n if (value instanceof Set) return value.has(permission);\n return false;\n}\n\n// Fail-closed operation permission (#2977, #3011): each applied op requires\n// the same \\`<collection>.<op>\\` permission the mirrored generated write route\n// enforces, read from the principal's session permission snapshot. A missing\n// snapshot denies; only an explicit tenancy super-admin bypass skips it.\nfunction hasOperationPermission(\n locals: unknown,\n permissionCollection: string,\n op: SyncApplyOp,\n): boolean {\n const l = readRecord(locals);\n const tenantContext = readRecord(l.tenantContext);\n if (tenantContext.superAdminBypass === true) return true;\n const permission = \\`\\${permissionCollection}.\\${op}\\`;\n return (\n hasGrantedPermission(l.permissions, permission) ||\n hasGrantedPermission(l.permissionSet, permission) ||\n hasGrantedPermission(l.smrtPermissions, permission) ||\n hasGrantedPermission(tenantContext.permissions, permission)\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: (op: SyncApplyOp) => {\n // \\`public: true\\` keeps its unauthenticated write semantics, as on\n // the generated CRUD routes.\n if (target.publicAccess === true) return 'ok' as const;\n if (!authenticated) return 'auth_required' as const;\n return hasOperationPermission(locals, target.permissionCollection, op)\n ? ('ok' as const)\n : ('forbidden' as const);\n },\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(\n routePath,\n generateSyncApplyRouteTemplate(\n targets,\n resolveSvelteKitConfigImport(projectRoot, routeDir, options),\n ),\n 'utf-8',\n );\n console.log(`[smrt] Generated: ${routePath}`);\n return true;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA+BA,IAAM,mBAAmB;CAAC;CAAU;CAAU;AAAQ;AA6BtD,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,4BAA4B,WAA0C;CAC7E,MAAM,aACJ,UAAU,iBACT;CACH,OAAO,OAAO,eAAe,YAAY,WAAW,SAAS,IACzD,aACA,UAAU;AAChB;;;;;AAMA,SAAgB,wBACd,UACkB;CAClB,MAAM,UAA4B,CAAC;CAEnC,KAAK,MAAM,CAAC,WAAW,cAAc,OAAO,QAAQ,SAAS,OAAO,GAAG;EACrE,IAAI,0BAA0B,UAAU,SAAS,GAAG;EAIpD,IAAI,qBAAqB,UAAU,WAAW,UAAU,WAAW,GACjE;EAGF,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,sBAAsB,4BAA4B,SAAS;GAC3D;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;CAY3D,OAAO,MAXS,QAAQ,KAAK,WAAW;EACtC,MAAM,SAAS;GACb,gBAAgB,KAAK,UAAU,OAAO,WAAW;GACjD,yBAAyB,KAAK,UAAU,OAAO,oBAAoB;GACnE,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,SACA,cACQ;CACR,MAAM,kBAAkB,QAAQ,MAAM,WAAW,OAAO,YAAY;CAoBpE,OAAO,GAAG,4BAA4B;;;;;;;;;;;;;;iCAcP,aAAa;;EAhCvB,kBACjB;;;;;;;;;;;;;;IAeA,GAkBS;;;;;;;;;;;;;;yDAc0C,iBAAiB,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmDjF,kBAAkB,wCAAwC,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4C/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,cACE,WACA,+BACE,SACA,6BAA6B,aAAa,UAAU,OAAO,CAC7D,GACA,OACF;CACA,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 { isFrameworkBaseClass } from '../registry/framework-base-classes.js';\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 { generateCollectionAccessImports } from './sveltekit-config-import.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 /**\n * `<collection>` prefix of the `<collection>.<action>` operation permission\n * the mirrored generated write routes require (#2977) — the same\n * `decoratorConfig.collection ?? collection` resolution as\n * sveltekit-generator.ts `resolvePermissionCollection`.\n */\n permissionCollection: 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 `resolvePermissionCollection` (#2977). */\nfunction resolvePermissionCollection(objectDef: SmartObjectDefinition): string {\n const configured = (\n objectDef.decoratorConfig as { collection?: unknown } | undefined\n )?.collection;\n return typeof configured === 'string' && configured.length > 0\n ? configured\n : objectDef.collection;\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 // The framework's own abstract base classes are scaffolding, not\n // resources — never make them a sync-apply target, regardless of\n // config (#2642).\n if (isFrameworkBaseClass(objectDef.className, objectDef.packageName)) {\n continue;\n }\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 permissionCollection: resolvePermissionCollection(objectDef),\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 `permissionCollection: ${JSON.stringify(target.permissionCollection)}`,\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 collectionAccessImports: string,\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 type { RequestHandler } from './$types';\n${collectionAccessImports}\n${tenantHelper}\ninterface SyncTargetConfig {\n /** Manifest registry key (may be package-qualified), as CRUD routes use. */\n registryKey: string;\n /** \\`<collection>\\` prefix of the required \\`<collection>.<op>\\` permission. */\n permissionCollection: 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\nfunction readRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object'\n ? (value as Record<string, unknown>)\n : {};\n}\n\nfunction hasGrantedPermission(value: unknown, permission: string): boolean {\n if (Array.isArray(value)) return value.includes(permission);\n if (value instanceof Set) return value.has(permission);\n return false;\n}\n\n// Fail-closed operation permission (#2977, #3011): each applied op requires\n// the same \\`<collection>.<op>\\` permission the mirrored generated write route\n// enforces, read from the principal's session permission snapshot. A missing\n// snapshot denies; only an explicit tenancy super-admin bypass skips it.\nfunction hasOperationPermission(\n locals: unknown,\n permissionCollection: string,\n op: SyncApplyOp,\n): boolean {\n const l = readRecord(locals);\n const tenantContext = readRecord(l.tenantContext);\n if (tenantContext.superAdminBypass === true) return true;\n const permission = \\`\\${permissionCollection}.\\${op}\\`;\n return (\n hasGrantedPermission(l.permissions, permission) ||\n hasGrantedPermission(l.permissionSet, permission) ||\n hasGrantedPermission(l.smrtPermissions, permission) ||\n hasGrantedPermission(tenantContext.permissions, permission)\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: (op: SyncApplyOp) => {\n // \\`public: true\\` keeps its unauthenticated write semantics, as on\n // the generated CRUD routes.\n if (target.publicAccess === true) return 'ok' as const;\n if (!authenticated) return 'auth_required' as const;\n return hasOperationPermission(locals, target.permissionCollection, op)\n ? ('ok' as const)\n : ('forbidden' as const);\n },\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(\n routePath,\n generateSyncApplyRouteTemplate(\n targets,\n generateCollectionAccessImports(projectRoot, routeDir, options, [\n 'getCollection',\n ]),\n ),\n 'utf-8',\n );\n console.log(`[smrt] Generated: ${routePath}`);\n return true;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-core",
|
|
3
|
-
"version": "0.53.
|
|
3
|
+
"version": "0.53.6",
|
|
4
4
|
"description": "Core AI agent framework with standardized collections, object-relational mapping, and code generators",
|
|
5
5
|
"author": "HappyVertical",
|
|
6
6
|
"type": "module",
|
|
@@ -155,15 +155,15 @@
|
|
|
155
155
|
}
|
|
156
156
|
},
|
|
157
157
|
"dependencies": {
|
|
158
|
-
"@happyvertical/ai": "^0.
|
|
159
|
-
"@happyvertical/files": "^0.
|
|
160
|
-
"@happyvertical/json": "^0.
|
|
161
|
-
"@happyvertical/logger": "^0.
|
|
162
|
-
"@happyvertical/smrt-config": "0.53.
|
|
163
|
-
"@happyvertical/smrt-scanner": "0.53.
|
|
164
|
-
"@happyvertical/smrt-types": "0.53.
|
|
165
|
-
"@happyvertical/sql": "^0.
|
|
166
|
-
"@happyvertical/utils": "^0.
|
|
158
|
+
"@happyvertical/ai": "^0.100.3",
|
|
159
|
+
"@happyvertical/files": "^0.100.3",
|
|
160
|
+
"@happyvertical/json": "^0.100.3",
|
|
161
|
+
"@happyvertical/logger": "^0.100.3",
|
|
162
|
+
"@happyvertical/smrt-config": "0.53.6",
|
|
163
|
+
"@happyvertical/smrt-scanner": "0.53.6",
|
|
164
|
+
"@happyvertical/smrt-types": "0.53.6",
|
|
165
|
+
"@happyvertical/sql": "^0.100.3",
|
|
166
|
+
"@happyvertical/utils": "^0.100.3",
|
|
167
167
|
"@oxc-project/runtime": "^0.150.0",
|
|
168
168
|
"cheerio": "^1.2.0",
|
|
169
169
|
"fast-glob": "3.3.3",
|