@happyvertical/smrt-core 0.37.7 → 0.37.9
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 +25 -6
- package/dist/change-feed.d.ts +261 -0
- package/dist/change-feed.d.ts.map +1 -0
- package/dist/change-feed.js +482 -0
- package/dist/change-feed.js.map +1 -0
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +4 -2
- package/dist/class.js.map +1 -1
- package/dist/collection.js +2 -2
- package/dist/consumer-plugin/index.d.ts.map +1 -1
- package/dist/consumer-plugin/index.js +24 -19
- package/dist/consumer-plugin/index.js.map +1 -1
- package/dist/dispatch/bus.js +2 -2
- package/dist/dispatch/index.d.ts +1 -1
- package/dist/dispatch/index.d.ts.map +1 -1
- package/dist/dispatch/index.js +2 -2
- package/dist/dispatch/tenant-resolver.d.ts +23 -0
- package/dist/dispatch/tenant-resolver.d.ts.map +1 -1
- package/dist/dispatch/tenant-resolver.js +33 -1
- package/dist/dispatch/tenant-resolver.js.map +1 -1
- package/dist/generated-client-runtime.d.ts +26 -0
- package/dist/generated-client-runtime.d.ts.map +1 -0
- package/dist/generated-client-runtime.js +88 -0
- package/dist/generated-client-runtime.js.map +1 -0
- package/dist/generators/changes-route.d.ts +52 -0
- package/dist/generators/changes-route.d.ts.map +1 -0
- package/dist/generators/changes-route.js +111 -0
- package/dist/generators/changes-route.js.map +1 -0
- package/dist/generators/conditional-get.d.ts +12 -0
- package/dist/generators/conditional-get.d.ts.map +1 -1
- package/dist/generators/conditional-get.js +30 -2
- package/dist/generators/conditional-get.js.map +1 -1
- package/dist/generators/rest.d.ts +58 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +114 -21
- package/dist/generators/rest.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/manifest/manifest-loader.d.ts +10 -1
- package/dist/manifest/manifest-loader.d.ts.map +1 -1
- package/dist/manifest/manifest-loader.js +18 -5
- package/dist/manifest/manifest-loader.js.map +1 -1
- 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.d.ts.map +1 -1
- package/dist/manifest/test-manifest-stub.js +880 -105
- package/dist/manifest/test-manifest-stub.js.map +1 -1
- package/dist/manifest.json +2 -2
- package/dist/object.js +2 -2
- package/dist/prebuild/index.d.ts.map +1 -1
- package/dist/prebuild/index.js +14 -7
- package/dist/prebuild/index.js.map +1 -1
- package/dist/registry/shared-state.d.ts +8 -1
- package/dist/registry/shared-state.d.ts.map +1 -1
- package/dist/registry/shared-state.js +11 -3
- package/dist/registry/shared-state.js.map +1 -1
- package/dist/smrt-knowledge.json +6 -6
- package/dist/system/schema.d.ts +24 -1
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +45 -3
- package/dist/system/schema.js.map +1 -1
- package/dist/testing/database.js +1 -1
- package/dist/utils/stack-frames.d.ts +50 -0
- package/dist/utils/stack-frames.d.ts.map +1 -0
- package/dist/utils/stack-frames.js +64 -0
- package/dist/utils/stack-frames.js.map +1 -0
- package/dist/vite-plugin/changes-route.d.ts +10 -0
- package/dist/vite-plugin/changes-route.d.ts.map +1 -0
- package/dist/vite-plugin/changes-route.js +174 -0
- package/dist/vite-plugin/changes-route.js.map +1 -0
- package/dist/vite-plugin/index.d.ts +7 -0
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +38 -20
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/route-header.d.ts +10 -0
- package/dist/vite-plugin/route-header.d.ts.map +1 -0
- package/dist/vite-plugin/route-header.js +14 -0
- package/dist/vite-plugin/route-header.js.map +1 -0
- package/dist/vite-plugin/sveltekit-generator.d.ts +7 -0
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +46 -20
- 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 -2
- package/dist/vite-plugin/sync-apply-route.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//#region src/utils/stack-frames.ts
|
|
2
|
+
/**
|
|
3
|
+
* Decorator-runtime stack-frame detection (#1785).
|
|
4
|
+
*
|
|
5
|
+
* ObjectRegistry attributes a `@smrt()` class to its declaring package by
|
|
6
|
+
* walking the registration call stack for the first frame OUTSIDE smrt-core —
|
|
7
|
+
* the module that applied the decorator. Decorator *lowering* breaks that
|
|
8
|
+
* assumption: the compiler inserts a runtime-helper frame between the `@smrt()`
|
|
9
|
+
* decorator and the consumer module that declares the class.
|
|
10
|
+
*
|
|
11
|
+
* Under Vite 8, oxc's legacy-decorator lowering routes the decorator application
|
|
12
|
+
* through `@oxc-project/runtime`'s helper (e.g.
|
|
13
|
+
* `.../@oxc-project/runtime/src/helpers/esm/applyDecoratedDescriptor.js`), so a
|
|
14
|
+
* naive walk stops at that frame and misattributes the class to
|
|
15
|
+
* `@oxc-project/runtime` instead of the consumer package. `tslib` (tsc's
|
|
16
|
+
* `__decorate`), `@swc/helpers`, and `@babel/runtime` insert the identical kind
|
|
17
|
+
* of frame under their respective toolchains.
|
|
18
|
+
*
|
|
19
|
+
* These are pure plumbing packages — a class is applied THROUGH them, never
|
|
20
|
+
* DECLARED in them — so both stack walks (source-file identity in
|
|
21
|
+
* `registry/shared-state.ts` and package attribution in
|
|
22
|
+
* `manifest/manifest-loader.ts`) skip them and continue to the real declaring
|
|
23
|
+
* module. Kept as a leaf module (no smrt imports) so both walkers can share it
|
|
24
|
+
* without risking an import cycle.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Runtime-helper packages that decorator lowering routes the decorator
|
|
28
|
+
* application through. Frames from these packages are never the module that
|
|
29
|
+
* declares a decorated class.
|
|
30
|
+
*/
|
|
31
|
+
var DECORATOR_RUNTIME_PACKAGES = [
|
|
32
|
+
"@oxc-project/runtime",
|
|
33
|
+
"@swc/helpers",
|
|
34
|
+
"@babel/runtime",
|
|
35
|
+
"tslib"
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Whether a file path (normalized to forward slashes) lives inside a
|
|
39
|
+
* decorator-runtime helper package — i.e. a stack frame introduced by decorator
|
|
40
|
+
* lowering rather than the module that declares the decorated class.
|
|
41
|
+
*
|
|
42
|
+
* Matches both the plain installed form (`.../node_modules/tslib/...`) and the
|
|
43
|
+
* pnpm form (`.../node_modules/.pnpm/tslib@2/node_modules/tslib/...`) because
|
|
44
|
+
* the real `/<package>/` segment appears in both. Case-insensitive.
|
|
45
|
+
*
|
|
46
|
+
* @param path - A file path from a stack frame (already normalized to `/`).
|
|
47
|
+
*/
|
|
48
|
+
function isDecoratorRuntimeFramePath(path) {
|
|
49
|
+
const lower = path.toLowerCase();
|
|
50
|
+
return DECORATOR_RUNTIME_PACKAGES.some((pkg) => lower.includes(`/${pkg}/`));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Whether a scoped package name (as captured from a `node_modules/@scope/name`
|
|
54
|
+
* stack match) is a decorator-runtime helper package.
|
|
55
|
+
*
|
|
56
|
+
* @param name - A scoped package name, e.g. `@oxc-project/runtime`.
|
|
57
|
+
*/
|
|
58
|
+
function isDecoratorRuntimePackageName(name) {
|
|
59
|
+
return DECORATOR_RUNTIME_PACKAGES.includes(name);
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { DECORATOR_RUNTIME_PACKAGES, isDecoratorRuntimeFramePath, isDecoratorRuntimePackageName };
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=stack-frames.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stack-frames.js","names":[],"sources":["../../src/utils/stack-frames.ts"],"sourcesContent":["/**\n * Decorator-runtime stack-frame detection (#1785).\n *\n * ObjectRegistry attributes a `@smrt()` class to its declaring package by\n * walking the registration call stack for the first frame OUTSIDE smrt-core —\n * the module that applied the decorator. Decorator *lowering* breaks that\n * assumption: the compiler inserts a runtime-helper frame between the `@smrt()`\n * decorator and the consumer module that declares the class.\n *\n * Under Vite 8, oxc's legacy-decorator lowering routes the decorator application\n * through `@oxc-project/runtime`'s helper (e.g.\n * `.../@oxc-project/runtime/src/helpers/esm/applyDecoratedDescriptor.js`), so a\n * naive walk stops at that frame and misattributes the class to\n * `@oxc-project/runtime` instead of the consumer package. `tslib` (tsc's\n * `__decorate`), `@swc/helpers`, and `@babel/runtime` insert the identical kind\n * of frame under their respective toolchains.\n *\n * These are pure plumbing packages — a class is applied THROUGH them, never\n * DECLARED in them — so both stack walks (source-file identity in\n * `registry/shared-state.ts` and package attribution in\n * `manifest/manifest-loader.ts`) skip them and continue to the real declaring\n * module. Kept as a leaf module (no smrt imports) so both walkers can share it\n * without risking an import cycle.\n */\n\n/**\n * Runtime-helper packages that decorator lowering routes the decorator\n * application through. Frames from these packages are never the module that\n * declares a decorated class.\n */\nexport const DECORATOR_RUNTIME_PACKAGES: readonly string[] = [\n '@oxc-project/runtime',\n '@swc/helpers',\n '@babel/runtime',\n 'tslib',\n];\n\n/**\n * Whether a file path (normalized to forward slashes) lives inside a\n * decorator-runtime helper package — i.e. a stack frame introduced by decorator\n * lowering rather than the module that declares the decorated class.\n *\n * Matches both the plain installed form (`.../node_modules/tslib/...`) and the\n * pnpm form (`.../node_modules/.pnpm/tslib@2/node_modules/tslib/...`) because\n * the real `/<package>/` segment appears in both. Case-insensitive.\n *\n * @param path - A file path from a stack frame (already normalized to `/`).\n */\nexport function isDecoratorRuntimeFramePath(path: string): boolean {\n const lower = path.toLowerCase();\n return DECORATOR_RUNTIME_PACKAGES.some((pkg) => lower.includes(`/${pkg}/`));\n}\n\n/**\n * Whether a scoped package name (as captured from a `node_modules/@scope/name`\n * stack match) is a decorator-runtime helper package.\n *\n * @param name - A scoped package name, e.g. `@oxc-project/runtime`.\n */\nexport function isDecoratorRuntimePackageName(name: string): boolean {\n return DECORATOR_RUNTIME_PACKAGES.includes(name);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,IAAa,6BAAgD;CAC3D;CACA;CACA;CACA;AACF;;;;;;;;;;;;AAaA,SAAgB,4BAA4B,MAAuB;CACjE,MAAM,QAAQ,KAAK,YAAY;CAC/B,OAAO,2BAA2B,MAAM,QAAQ,MAAM,SAAS,IAAI,IAAI,EAAE,CAAC;AAC5E;;;;;;;AAQA,SAAgB,8BAA8B,MAAuB;CACnE,OAAO,2BAA2B,SAAS,IAAI;AACjD"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SmartObjectManifest } from '../scanner/types';
|
|
2
|
+
import { SvelteKitOptions } from './sveltekit-generator.js';
|
|
3
|
+
/**
|
|
4
|
+
* Generate the `_changes/+server.ts` route. Returns true when a route was
|
|
5
|
+
* written. Disabled with `sveltekit: { changesRoute: { enabled: false } }`;
|
|
6
|
+
* skipped (with a log line) when the manifest has no objects to anchor the
|
|
7
|
+
* database on.
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateChangesRoute(projectRoot: string, manifest: SmartObjectManifest, options: SvelteKitOptions): boolean;
|
|
10
|
+
//# sourceMappingURL=changes-route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changes-route.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/changes-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,KAAK,EAEV,mBAAmB,EACpB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAyCjE;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CA0BT"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { AUTO_GENERATED_ROUTE_HEADER } from "./route-header.js";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
//#region src/vite-plugin/changes-route.ts
|
|
5
|
+
/**
|
|
6
|
+
* SvelteKit `_changes` route generation for the change feed (issue #1758).
|
|
7
|
+
*
|
|
8
|
+
* Emits `{routesDir}/_changes/+server.ts` — an auth-guarded, tenant-scoped
|
|
9
|
+
* GET endpoint over the `_smrt_changes` log, part of the client/mobile sync
|
|
10
|
+
* contract (PRD #1755). Kept in its own module so `sveltekit-generator.ts`
|
|
11
|
+
* only carries a one-line registration.
|
|
12
|
+
*
|
|
13
|
+
* Design notes:
|
|
14
|
+
* - **Fail-closed auth** (#1540 posture): the handler requires an
|
|
15
|
+
* authenticated principal on `locals`. The feed spans every table, so
|
|
16
|
+
* per-model `api: { public }` opt-outs deliberately do not apply.
|
|
17
|
+
* - **Tenant scoping**: when the project has tenant-scoped objects, the
|
|
18
|
+
* route establishes tenant context from `locals` exactly like generated
|
|
19
|
+
* collection routes, then reads through
|
|
20
|
+
* `getTenantScopedChangesSince()` — a tenant only ever sees its own
|
|
21
|
+
* changes plus global rows.
|
|
22
|
+
* - **Database resolution**: the route anchors on the project's first
|
|
23
|
+
* generated collection (alphabetical) via the consumer's existing
|
|
24
|
+
* `getCollection()` helper, inheriting its configuration, request-scoped
|
|
25
|
+
* database support and system-table bootstrap. Multi-database projects
|
|
26
|
+
* (per-object `db` overrides) see the anchor collection's feed.
|
|
27
|
+
* - Cleanup rides the existing generated-route sweep: the emitted file
|
|
28
|
+
* starts with {@link AUTO_GENERATED_ROUTE_HEADER}.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Mirrors `isCollectionClass` in `sveltekit-generator.ts` (module-private
|
|
32
|
+
* there): collection classes share route paths with their item class and
|
|
33
|
+
* never anchor routes themselves.
|
|
34
|
+
*/
|
|
35
|
+
function isCollectionDefinition(objectDef) {
|
|
36
|
+
return objectDef.extends === "SmrtCollection" || !!objectDef.extendsTypeArg;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Pick the class the route resolves its database through: the first
|
|
40
|
+
* non-collection object by sorted manifest key, for determinism across
|
|
41
|
+
* builds.
|
|
42
|
+
*
|
|
43
|
+
* Returns the manifest **registry key** verbatim (which may be
|
|
44
|
+
* package-qualified, e.g. `@happyvertical/smrt-ledgers:Account`) — the emitted
|
|
45
|
+
* route passes it straight to `getCollection()`, exactly as the generated CRUD
|
|
46
|
+
* routes do. Collapsing it to a simple class name would resolve ambiguously
|
|
47
|
+
* when two loaded packages declare the same simple name (mirrors the #1778
|
|
48
|
+
* verbatim-key fix and the sync-apply route's `registryKey`).
|
|
49
|
+
*/
|
|
50
|
+
function resolveAnchorClassName(manifest) {
|
|
51
|
+
const sortedNames = Object.keys(manifest.objects).sort();
|
|
52
|
+
for (const name of sortedNames) {
|
|
53
|
+
const def = manifest.objects[name];
|
|
54
|
+
if (def && !isCollectionDefinition(def)) return name;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function manifestHasTenantScopedObject(manifest) {
|
|
59
|
+
return Object.values(manifest.objects).some((def) => !isCollectionDefinition(def) && !!def.decoratorConfig?.tenantScoped);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Generate the `_changes/+server.ts` route. Returns true when a route was
|
|
63
|
+
* written. Disabled with `sveltekit: { changesRoute: { enabled: false } }`;
|
|
64
|
+
* skipped (with a log line) when the manifest has no objects to anchor the
|
|
65
|
+
* database on.
|
|
66
|
+
*/
|
|
67
|
+
function generateChangesRoute(projectRoot, manifest, options) {
|
|
68
|
+
if (options.changesRoute?.enabled === false) return false;
|
|
69
|
+
const anchorClassName = resolveAnchorClassName(manifest);
|
|
70
|
+
if (!anchorClassName) {
|
|
71
|
+
console.log("[smrt] Skipping _changes route - no SMRT objects to anchor the database on");
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const routeDir = join(projectRoot, options.routesDir, "_changes");
|
|
75
|
+
const content = generateChangesRouteTemplate(anchorClassName, manifestHasTenantScopedObject(manifest));
|
|
76
|
+
if (!existsSync(routeDir)) mkdirSync(routeDir, { recursive: true });
|
|
77
|
+
const filePath = join(routeDir, "+server.ts");
|
|
78
|
+
writeFileSync(filePath, content, "utf-8");
|
|
79
|
+
console.log(`[smrt] Generated: ${filePath}`);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
function generateChangesRouteTemplate(anchorClassName, tenantScoped) {
|
|
83
|
+
return `${AUTO_GENERATED_ROUTE_HEADER}
|
|
84
|
+
// DO NOT EDIT - changes will be overwritten
|
|
85
|
+
//
|
|
86
|
+
// GET /_changes — cursor read over the _smrt_changes change feed (#1758).
|
|
87
|
+
// Part of the client/mobile sync contract: poll with the returned cursor to
|
|
88
|
+
// observe every committed framework save/delete (deletes are tombstones)
|
|
89
|
+
// exactly once. Query params: since (cursor, default 0), tables
|
|
90
|
+
// (comma-separated), limit. A response with resyncRequired: true (still
|
|
91
|
+
// HTTP 200 — protocol state, not an error) means the cursor cannot be
|
|
92
|
+
// served incrementally (pruned or foreign) and the client must re-fetch
|
|
93
|
+
// in full before resuming polling.
|
|
94
|
+
|
|
95
|
+
import { error, json } from '@sveltejs/kit';
|
|
96
|
+
import { getTenantScopedChangesSince } from '@happyvertical/smrt-core';
|
|
97
|
+
import { getCollection } from '$lib/server/smrt';
|
|
98
|
+
import type { RequestHandler } from './$types';
|
|
99
|
+
|
|
100
|
+
// Fail-closed authorization (#1540): the change feed spans every table, so
|
|
101
|
+
// it is never public — an authenticated principal on \`locals\` is required.
|
|
102
|
+
function hasAuthenticatedPrincipal(locals: unknown): boolean {
|
|
103
|
+
if (!locals || typeof locals !== 'object') return false;
|
|
104
|
+
const l = locals as Record<string, unknown>;
|
|
105
|
+
const isResolvedPrincipal = (v: unknown) =>
|
|
106
|
+
typeof v === 'object' && v !== null;
|
|
107
|
+
return (
|
|
108
|
+
isResolvedPrincipal(l.user) ||
|
|
109
|
+
isResolvedPrincipal(l.session) ||
|
|
110
|
+
l.smrtAuth === true
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function requireRouteAuth(locals: unknown): void {
|
|
115
|
+
if (!hasAuthenticatedPrincipal(locals)) {
|
|
116
|
+
throw error(401, 'Authentication required');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
${tenantScoped ? `
|
|
120
|
+
import { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenancy';
|
|
121
|
+
|
|
122
|
+
function establishTenantContext(locals: unknown): void {
|
|
123
|
+
if (hasTenantContext()) return;
|
|
124
|
+
if (!locals || typeof locals !== 'object') return;
|
|
125
|
+
const l = locals as Record<string, unknown>;
|
|
126
|
+
const user = l.user as Record<string, unknown> | undefined;
|
|
127
|
+
const session = l.session as Record<string, unknown> | undefined;
|
|
128
|
+
const tenantId = l.tenantId ?? user?.tenantId ?? session?.tenantId;
|
|
129
|
+
if (typeof tenantId === 'string' && tenantId) {
|
|
130
|
+
enterTenantContext({ tenantId });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
` : ""}
|
|
134
|
+
export const GET: RequestHandler = async ({ locals, url }) => {
|
|
135
|
+
requireRouteAuth(locals);${tenantScoped ? "\n establishTenantContext(locals);" : ""}
|
|
136
|
+
|
|
137
|
+
const since = Number(url.searchParams.get('since') ?? '0');
|
|
138
|
+
if (!Number.isFinite(since) || since < 0) {
|
|
139
|
+
throw error(400, "'since' must be a non-negative number");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let limit: number | undefined;
|
|
143
|
+
const limitParam = url.searchParams.get('limit');
|
|
144
|
+
if (limitParam !== null) {
|
|
145
|
+
limit = Number(limitParam);
|
|
146
|
+
if (!Number.isFinite(limit) || limit < 1) {
|
|
147
|
+
throw error(400, "'limit' must be a positive number");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const tablesParam = url.searchParams.get('tables');
|
|
152
|
+
const tables = tablesParam
|
|
153
|
+
? tablesParam
|
|
154
|
+
.split(',')
|
|
155
|
+
.map((table) => table.trim())
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
: undefined;
|
|
158
|
+
|
|
159
|
+
// The feed lives in the project's database; anchor on the
|
|
160
|
+
// ${anchorClassName} collection to reuse its configured connection.
|
|
161
|
+
const collection = await getCollection('${anchorClassName}');
|
|
162
|
+
const page = await getTenantScopedChangesSince(collection.db, {
|
|
163
|
+
since,
|
|
164
|
+
tables,
|
|
165
|
+
limit,
|
|
166
|
+
});
|
|
167
|
+
return json(page);
|
|
168
|
+
};
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
export { generateChangesRoute };
|
|
173
|
+
|
|
174
|
+
//# sourceMappingURL=changes-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changes-route.js","names":[],"sources":["../../src/vite-plugin/changes-route.ts"],"sourcesContent":["/**\n * SvelteKit `_changes` route generation for the change feed (issue #1758).\n *\n * Emits `{routesDir}/_changes/+server.ts` — an auth-guarded, tenant-scoped\n * GET endpoint over the `_smrt_changes` log, part of the client/mobile sync\n * contract (PRD #1755). Kept in its own module so `sveltekit-generator.ts`\n * only carries a one-line registration.\n *\n * Design notes:\n * - **Fail-closed auth** (#1540 posture): the handler requires an\n * authenticated principal on `locals`. The feed spans every table, so\n * per-model `api: { public }` opt-outs deliberately do not apply.\n * - **Tenant scoping**: when the project has tenant-scoped objects, the\n * route establishes tenant context from `locals` exactly like generated\n * collection routes, then reads through\n * `getTenantScopedChangesSince()` — a tenant only ever sees its own\n * changes plus global rows.\n * - **Database resolution**: the route anchors on the project's first\n * generated collection (alphabetical) via the consumer's existing\n * `getCollection()` helper, inheriting its configuration, request-scoped\n * database support and system-table bootstrap. Multi-database projects\n * (per-object `db` overrides) see the anchor collection's feed.\n * - Cleanup rides the existing generated-route sweep: the emitted file\n * starts with {@link AUTO_GENERATED_ROUTE_HEADER}.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\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\n/**\n * Mirrors `isCollectionClass` in `sveltekit-generator.ts` (module-private\n * there): collection classes share route paths with their item class and\n * never anchor routes themselves.\n */\nfunction isCollectionDefinition(objectDef: SmartObjectDefinition): boolean {\n return objectDef.extends === 'SmrtCollection' || !!objectDef.extendsTypeArg;\n}\n\n/**\n * Pick the class the route resolves its database through: the first\n * non-collection object by sorted manifest key, for determinism across\n * builds.\n *\n * Returns the manifest **registry key** verbatim (which may be\n * package-qualified, e.g. `@happyvertical/smrt-ledgers:Account`) — the emitted\n * route passes it straight to `getCollection()`, exactly as the generated CRUD\n * routes do. Collapsing it to a simple class name would resolve ambiguously\n * when two loaded packages declare the same simple name (mirrors the #1778\n * verbatim-key fix and the sync-apply route's `registryKey`).\n */\nfunction resolveAnchorClassName(manifest: SmartObjectManifest): string | null {\n const sortedNames = Object.keys(manifest.objects).sort();\n for (const name of sortedNames) {\n const def = manifest.objects[name];\n if (def && !isCollectionDefinition(def)) {\n return name;\n }\n }\n return null;\n}\n\nfunction manifestHasTenantScopedObject(manifest: SmartObjectManifest): boolean {\n return Object.values(manifest.objects).some(\n (def) =>\n !isCollectionDefinition(def) && !!def.decoratorConfig?.tenantScoped,\n );\n}\n\n/**\n * Generate the `_changes/+server.ts` route. Returns true when a route was\n * written. Disabled with `sveltekit: { changesRoute: { enabled: false } }`;\n * skipped (with a log line) when the manifest has no objects to anchor the\n * database on.\n */\nexport function generateChangesRoute(\n projectRoot: string,\n manifest: SmartObjectManifest,\n options: SvelteKitOptions,\n): boolean {\n if (options.changesRoute?.enabled === false) {\n return false;\n }\n\n const anchorClassName = resolveAnchorClassName(manifest);\n if (!anchorClassName) {\n console.log(\n '[smrt] Skipping _changes route - no SMRT objects to anchor the database on',\n );\n return false;\n }\n\n const routeDir = join(projectRoot, options.routesDir, '_changes');\n const content = generateChangesRouteTemplate(\n anchorClassName,\n manifestHasTenantScopedObject(manifest),\n );\n\n if (!existsSync(routeDir)) {\n mkdirSync(routeDir, { recursive: true });\n }\n const filePath = join(routeDir, '+server.ts');\n writeFileSync(filePath, content, 'utf-8');\n console.log(`[smrt] Generated: ${filePath}`);\n return true;\n}\n\nfunction generateChangesRouteTemplate(\n anchorClassName: string,\n tenantScoped: boolean,\n): string {\n const tenantHelper = tenantScoped\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 const tenantCall = tenantScoped ? '\\n establishTenantContext(locals);' : '';\n\n return `${AUTO_GENERATED_ROUTE_HEADER}\n// DO NOT EDIT - changes will be overwritten\n//\n// GET /_changes — cursor read over the _smrt_changes change feed (#1758).\n// Part of the client/mobile sync contract: poll with the returned cursor to\n// observe every committed framework save/delete (deletes are tombstones)\n// exactly once. Query params: since (cursor, default 0), tables\n// (comma-separated), limit. A response with resyncRequired: true (still\n// HTTP 200 — protocol state, not an error) means the cursor cannot be\n// served incrementally (pruned or foreign) and the client must re-fetch\n// in full before resuming polling.\n\nimport { error, json } from '@sveltejs/kit';\nimport { getTenantScopedChangesSince } from '@happyvertical/smrt-core';\nimport { getCollection } from '$lib/server/smrt';\nimport type { RequestHandler } from './$types';\n\n// Fail-closed authorization (#1540): the change feed spans every table, so\n// it is never public — an authenticated principal on \\`locals\\` is required.\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 requireRouteAuth(locals: unknown): void {\n if (!hasAuthenticatedPrincipal(locals)) {\n throw error(401, 'Authentication required');\n }\n}\n${tenantHelper}\nexport const GET: RequestHandler = async ({ locals, url }) => {\n requireRouteAuth(locals);${tenantCall}\n\n const since = Number(url.searchParams.get('since') ?? '0');\n if (!Number.isFinite(since) || since < 0) {\n throw error(400, \"'since' must be a non-negative number\");\n }\n\n let limit: number | undefined;\n const limitParam = url.searchParams.get('limit');\n if (limitParam !== null) {\n limit = Number(limitParam);\n if (!Number.isFinite(limit) || limit < 1) {\n throw error(400, \"'limit' must be a positive number\");\n }\n }\n\n const tablesParam = url.searchParams.get('tables');\n const tables = tablesParam\n ? tablesParam\n .split(',')\n .map((table) => table.trim())\n .filter(Boolean)\n : undefined;\n\n // The feed lives in the project's database; anchor on the\n // ${anchorClassName} collection to reuse its configured connection.\n const collection = await getCollection('${anchorClassName}');\n const page = await getTenantScopedChangesSince(collection.db, {\n since,\n tables,\n limit,\n });\n return json(page);\n};\n`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAS,uBAAuB,WAA2C;CACzE,OAAO,UAAU,YAAY,oBAAoB,CAAC,CAAC,UAAU;AAC/D;;;;;;;;;;;;;AAcA,SAAS,uBAAuB,UAA8C;CAC5E,MAAM,cAAc,OAAO,KAAK,SAAS,OAAO,CAAC,CAAC,KAAK;CACvD,KAAK,MAAM,QAAQ,aAAa;EAC9B,MAAM,MAAM,SAAS,QAAQ;EAC7B,IAAI,OAAO,CAAC,uBAAuB,GAAG,GACpC,OAAO;CAEX;CACA,OAAO;AACT;AAEA,SAAS,8BAA8B,UAAwC;CAC7E,OAAO,OAAO,OAAO,SAAS,OAAO,CAAC,CAAC,MACpC,QACC,CAAC,uBAAuB,GAAG,KAAK,CAAC,CAAC,IAAI,iBAAiB,YAC3D;AACF;;;;;;;AAQA,SAAgB,qBACd,aACA,UACA,SACS;CACT,IAAI,QAAQ,cAAc,YAAY,OACpC,OAAO;CAGT,MAAM,kBAAkB,uBAAuB,QAAQ;CACvD,IAAI,CAAC,iBAAiB;EACpB,QAAQ,IACN,4EACF;EACA,OAAO;CACT;CAEA,MAAM,WAAW,KAAK,aAAa,QAAQ,WAAW,UAAU;CAChE,MAAM,UAAU,6BACd,iBACA,8BAA8B,QAAQ,CACxC;CAEA,IAAI,CAAC,WAAW,QAAQ,GACtB,UAAU,UAAU,EAAE,WAAW,KAAK,CAAC;CAEzC,MAAM,WAAW,KAAK,UAAU,YAAY;CAC5C,cAAc,UAAU,SAAS,OAAO;CACxC,QAAQ,IAAI,qBAAqB,UAAU;CAC3C,OAAO;AACT;AAEA,SAAS,6BACP,iBACA,cACQ;CAoBR,OAAO,GAAG,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAnBjB,eACjB;;;;;;;;;;;;;;IAeA,GAuCS;;6BAtCM,eAAe,wCAAwC,GAwCpC;;;;;;;;;;;;;;;;;;;;;;;;;OAyBjC,gBAAgB;4CACqB,gBAAgB;;;;;;;;;AAS5D"}
|
|
@@ -46,6 +46,13 @@ export interface SmrtPluginOptions {
|
|
|
46
46
|
* the next major. An explicit `api.routes[name].path` always wins.
|
|
47
47
|
*/
|
|
48
48
|
kebabRoutes?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Change-feed `_changes` route generation (#1758). Enabled by default
|
|
51
|
+
* (the route is auth-guarded fail-closed); `{ enabled: false }` skips it.
|
|
52
|
+
*/
|
|
53
|
+
changesRoute?: {
|
|
54
|
+
enabled?: boolean;
|
|
55
|
+
};
|
|
49
56
|
};
|
|
50
57
|
/** Domain-scoped agent/developer knowledge artifact generation. */
|
|
51
58
|
knowledge?: DomainKnowledgeConfig | false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EACV,qBAAqB,EAEtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EACV,qBAAqB,EAEtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,MAAM,CAAC;AAIlE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAa5D,YAAY,EACV,wBAAwB,EACxB,gBAAgB,GACjB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,6BAA6B,EAC7B,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,4BAA4B,GAC7B,MAAM,0BAA0B,CAAC;AAalC,MAAM,WAAW,iBAAiB;IAChC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,0BAA0B;IAC1B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,6BAA6B;IAC7B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qBAAqB;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,kFAAkF;IAClF,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,4EAA4E;IAC5E,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wEAAwE;IACxE,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpC;;OAEG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,8CAA8C;IAC9C,SAAS,CAAC,EAAE;QACV,yDAAyD;QACzD,OAAO,EAAE,OAAO,CAAC;QACjB,wEAAwE;QACxE,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,qEAAqE;QACrE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,mEAAmE;QACnE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,mDAAmD;QACnD,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB;;;;WAIG;QACH,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB;;;WAGG;QACH,YAAY,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,OAAO,CAAA;SAAE,CAAC;KACtC,CAAC;IACF,mEAAmE;IACnE,SAAS,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC;IAC1C;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AA8BD,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAG1D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,mBAAmB,GAC5B,MAAM,CAaR;AAED,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,MAAM,CA84BlE"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildDomainKnowledgeManifest } from "../knowledge.js";
|
|
2
2
|
import { importWorkspaceModule } from "../utils/import-workspace-module.js";
|
|
3
3
|
import { discoverSmrtPackages } from "../manifest/discover-smrt-packages.js";
|
|
4
|
+
import { CLIENT_FETCH_RUNTIME } from "../generated-client-runtime.js";
|
|
4
5
|
import { importBuildAwareModule } from "./import-build-aware.js";
|
|
5
6
|
import { findCliApiCoherenceViolations, generateSvelteKitRoutes, methodNameToKebab, resolveApiActionSet, validateCliIncludeAgainstApi } from "./sveltekit-generator.js";
|
|
6
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -256,6 +257,7 @@ function smrtPlugin(options = {}) {
|
|
|
256
257
|
configPath: svelteKit.configPath || "src/lib/server",
|
|
257
258
|
configFileName: svelteKit.configFileName || "smrt.ts",
|
|
258
259
|
kebabRoutes: svelteKit.kebabRoutes ?? false,
|
|
260
|
+
changesRoute: svelteKit.changesRoute,
|
|
259
261
|
knowledge: await resolveKnowledgeConfig(resolvedConfig.root, manifest)
|
|
260
262
|
});
|
|
261
263
|
},
|
|
@@ -319,6 +321,7 @@ function smrtPlugin(options = {}) {
|
|
|
319
321
|
configPath: svelteKit.configPath || "src/lib/server",
|
|
320
322
|
configFileName: svelteKit.configFileName || "smrt.ts",
|
|
321
323
|
kebabRoutes: svelteKit.kebabRoutes ?? false,
|
|
324
|
+
changesRoute: svelteKit.changesRoute,
|
|
322
325
|
knowledge: await resolveKnowledgeConfig(server.config.root, manifest)
|
|
323
326
|
});
|
|
324
327
|
Object.values(VIRTUAL_MODULES).forEach((id) => {
|
|
@@ -346,6 +349,7 @@ function smrtPlugin(options = {}) {
|
|
|
346
349
|
configPath: svelteKit.configPath || "src/lib/server",
|
|
347
350
|
configFileName: svelteKit.configFileName || "smrt.ts",
|
|
348
351
|
kebabRoutes: svelteKit.kebabRoutes ?? false,
|
|
352
|
+
changesRoute: svelteKit.changesRoute,
|
|
349
353
|
knowledge: await resolveKnowledgeConfig(server.config.root, manifest)
|
|
350
354
|
});
|
|
351
355
|
}
|
|
@@ -369,6 +373,7 @@ function smrtPlugin(options = {}) {
|
|
|
369
373
|
configPath: svelteKit.configPath || "src/lib/server",
|
|
370
374
|
configFileName: svelteKit.configFileName || "smrt.ts",
|
|
371
375
|
kebabRoutes: svelteKit.kebabRoutes ?? false,
|
|
376
|
+
changesRoute: svelteKit.changesRoute,
|
|
372
377
|
knowledge: await resolveKnowledgeConfig(server.config.root, manifest)
|
|
373
378
|
});
|
|
374
379
|
}
|
|
@@ -600,6 +605,8 @@ function generateClientModule(manifest, options = {}) {
|
|
|
600
605
|
// Auto-generated API client from SMRT objects
|
|
601
606
|
// This file is generated automatically - do not edit
|
|
602
607
|
|
|
608
|
+
${CLIENT_FETCH_RUNTIME}
|
|
609
|
+
|
|
603
610
|
export function createClient(basePath = '/api/v1') {
|
|
604
611
|
return {${uniqueApiClientEntries(Object.entries(manifest.objects)).map(({ obj, clientKey }) => {
|
|
605
612
|
const { collection, methods = {} } = obj;
|
|
@@ -616,45 +623,45 @@ export function createClient(basePath = '/api/v1') {
|
|
|
616
623
|
return options.kebabRoutes ? methodNameToKebab(methodName) : methodName;
|
|
617
624
|
};
|
|
618
625
|
const customMethodImpls = customMethods.map(([methodName, _method]) => {
|
|
619
|
-
return ` ${methodName}: (id, options) =>
|
|
626
|
+
return ` ${methodName}: (id, options) => __smrtFetchJson(basePath + '/${collection}/' + id + '/${segmentFor(methodName)}', {
|
|
620
627
|
method: 'POST',
|
|
621
628
|
headers: { 'Content-Type': 'application/json' },
|
|
622
629
|
body: JSON.stringify(options || {})
|
|
623
|
-
})
|
|
630
|
+
})`;
|
|
624
631
|
}).join(",\n");
|
|
625
632
|
return `
|
|
626
633
|
${clientKey}: {
|
|
627
|
-
list: (params) =>
|
|
634
|
+
list: (params) => __smrtFetchJson(basePath + '/${collection}', {
|
|
628
635
|
method: 'GET',
|
|
629
636
|
headers: { 'Content-Type': 'application/json' }
|
|
630
|
-
})
|
|
637
|
+
}),
|
|
631
638
|
|
|
632
|
-
get: (id) =>
|
|
639
|
+
get: (id) => __smrtFetchJson(basePath + '/${collection}/' + id, {
|
|
633
640
|
method: 'GET',
|
|
634
641
|
headers: { 'Content-Type': 'application/json' }
|
|
635
|
-
})
|
|
642
|
+
}),
|
|
636
643
|
|
|
637
|
-
create: (data) =>
|
|
644
|
+
create: (data) => __smrtFetchJson(basePath + '/${collection}', {
|
|
638
645
|
method: 'POST',
|
|
639
646
|
headers: { 'Content-Type': 'application/json' },
|
|
640
647
|
body: JSON.stringify(data)
|
|
641
|
-
})
|
|
648
|
+
}),
|
|
642
649
|
|
|
643
|
-
update: (id, data) =>
|
|
650
|
+
update: (id, data) => __smrtFetchJson(basePath + '/${collection}/' + id, {
|
|
644
651
|
method: 'PUT',
|
|
645
652
|
headers: { 'Content-Type': 'application/json' },
|
|
646
653
|
body: JSON.stringify(data)
|
|
647
|
-
})
|
|
654
|
+
}),
|
|
648
655
|
|
|
649
|
-
delete: (id) =>
|
|
656
|
+
delete: (id) => __smrtFetchOk(basePath + '/${collection}/' + id, {
|
|
650
657
|
method: 'DELETE',
|
|
651
658
|
headers: { 'Content-Type': 'application/json' }
|
|
652
|
-
})
|
|
659
|
+
}),
|
|
653
660
|
|
|
654
|
-
search: (query) =>
|
|
661
|
+
search: (query) => __smrtFetchJson(basePath + '/${collection}/search?q=' + encodeURIComponent(query), {
|
|
655
662
|
method: 'GET',
|
|
656
663
|
headers: { 'Content-Type': 'application/json' }
|
|
657
|
-
})
|
|
664
|
+
})${customMethods.length > 0 ? `,\n${customMethodImpls}` : ""}
|
|
658
665
|
}`;
|
|
659
666
|
}).join(",")}
|
|
660
667
|
};
|
|
@@ -835,8 +842,8 @@ async function generateTypeDeclarationFile(manifest, projectRoot, typeDeclaratio
|
|
|
835
842
|
${Object.entries(obj.fields).map(([fieldName, field]) => {
|
|
836
843
|
return ` ${fieldName}${field.required === false ? "?" : ""}: ${mapTypeScriptType(field.type)};`;
|
|
837
844
|
}).join("\n")}
|
|
838
|
-
|
|
839
|
-
|
|
845
|
+
created_at?: string;
|
|
846
|
+
updated_at?: string;
|
|
840
847
|
}`;
|
|
841
848
|
}).join("\n\n");
|
|
842
849
|
const apiClientInterface = uniqueApiClientEntries(Object.entries(manifest.objects)).map(({ obj, clientKey }) => {
|
|
@@ -922,15 +929,26 @@ declare module '@happyvertical/smrt-virt-routes' {
|
|
|
922
929
|
export default setupRoutes;
|
|
923
930
|
}
|
|
924
931
|
|
|
925
|
-
// Client module - Auto-generated API client
|
|
932
|
+
// Client module - Auto-generated API client
|
|
933
|
+
// Wire-shape policy (#1797): the server returns BARE JSON — a bare array for
|
|
934
|
+
// list/search, a bare object for get/create/update — with snake_case field
|
|
935
|
+
// names (created_at, updated_at). These declarations match that shape (no
|
|
936
|
+
// envelope, no camelCase). Fetchers reject on non-2xx with a SmrtClientError
|
|
937
|
+
// (#1796). This is byte-identical to the prebuild declaration path.
|
|
926
938
|
declare module '@happyvertical/smrt-virt-client' {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
data?: T;
|
|
939
|
+
/** Shape of a JSON error body carried by a rejected request (SmrtClientError.body). */
|
|
940
|
+
export interface ApiError {
|
|
930
941
|
error?: string;
|
|
931
942
|
message?: string;
|
|
932
943
|
}
|
|
933
944
|
|
|
945
|
+
/** Typed error thrown by every fetcher on a non-2xx response (#1796). */
|
|
946
|
+
export interface SmrtClientError extends Error {
|
|
947
|
+
name: 'SmrtClientError';
|
|
948
|
+
status: number;
|
|
949
|
+
body?: ApiError | string;
|
|
950
|
+
}
|
|
951
|
+
|
|
934
952
|
export interface CrudOperations<T = any> {
|
|
935
953
|
list(params?: Record<string, any>): Promise<T[]>;
|
|
936
954
|
get(id: string): Promise<T>;
|