@happyvertical/smrt-core 0.38.2 → 0.38.3
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 +11 -2
- package/dist/change-feed.d.ts +19 -10
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +43 -13
- package/dist/change-feed.js.map +1 -1
- package/dist/change-signals.d.ts +83 -0
- package/dist/change-signals.d.ts.map +1 -0
- package/dist/change-signals.js +272 -0
- package/dist/change-signals.js.map +1 -0
- package/dist/collection-cache.d.ts +21 -0
- package/dist/collection-cache.d.ts.map +1 -1
- package/dist/collection-cache.js +0 -0
- package/dist/collection-cache.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/decorators/index.d.ts +8 -0
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/generators/changes-route.d.ts +15 -27
- package/dist/generators/changes-route.d.ts.map +1 -1
- package/dist/generators/changes-route.js +17 -7
- package/dist/generators/changes-route.js.map +1 -1
- package/dist/generators/cli.d.ts +3 -0
- package/dist/generators/cli.d.ts.map +1 -1
- package/dist/generators/cli.js +7 -4
- package/dist/generators/cli.js.map +1 -1
- package/dist/generators/conditional-get.d.ts +17 -6
- package/dist/generators/conditional-get.d.ts.map +1 -1
- package/dist/generators/conditional-get.js +19 -13
- package/dist/generators/conditional-get.js.map +1 -1
- package/dist/generators/events-route.d.ts +72 -0
- package/dist/generators/events-route.d.ts.map +1 -0
- package/dist/generators/events-route.js +234 -0
- package/dist/generators/events-route.js.map +1 -0
- package/dist/generators/index.d.ts +1 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +2 -1
- package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
- package/dist/generators/mcp-runtime-template.js +11 -5
- package/dist/generators/mcp-runtime-template.js.map +1 -1
- package/dist/generators/mcp.d.ts +3 -0
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +18 -9
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/rest.d.ts +7 -1
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +77 -13
- package/dist/generators/rest.js.map +1 -1
- package/dist/generators.js +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/manifest/generator.js.map +1 -1
- package/dist/manifest/manifest-loader.js.map +1 -1
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +7 -3
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
- package/dist/manifest/test-manifest-stub.js +1050 -124
- package/dist/manifest/test-manifest-stub.js.map +1 -1
- package/dist/manifest.json +9 -3
- package/dist/mcp-advisor/tools/preview-api-endpoints.d.ts +1 -0
- package/dist/mcp-advisor/tools/preview-api-endpoints.d.ts.map +1 -1
- package/dist/mcp-advisor/types.d.ts +3 -0
- package/dist/mcp-advisor/types.d.ts.map +1 -1
- package/dist/migrations/tracker.js +5 -4
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations/types.d.ts +1 -1
- package/dist/migrations/types.d.ts.map +1 -1
- package/dist/object.d.ts +17 -8
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +42 -26
- package/dist/object.js.map +1 -1
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +12 -0
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/shared-state.d.ts +2 -1
- package/dist/registry/shared-state.d.ts.map +1 -1
- package/dist/registry/shared-state.js.map +1 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +1 -1
- package/dist/registry.js.map +1 -1
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js.map +1 -1
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +7 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/schema/ddl/base-strategy.js +1 -1
- package/dist/schema/ddl/base-strategy.js.map +1 -1
- package/dist/scripts/migrate-datetime-to-timestamp.ts +1 -1
- package/dist/signals/sanitizer.d.ts.map +1 -1
- package/dist/signals/sanitizer.js.map +1 -1
- package/dist/smrt-knowledge.json +8 -8
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/notifications-stub.d.ts +24 -0
- package/dist/testing/notifications-stub.d.ts.map +1 -0
- package/dist/testing/notifications-stub.js +62 -0
- package/dist/testing/notifications-stub.js.map +1 -0
- package/dist/testing.js +2 -1
- package/dist/tools/tool-generator.js +1 -1
- package/dist/tools/tool-generator.js.map +1 -1
- package/dist/vite-plugin/changes-route.d.ts +14 -0
- package/dist/vite-plugin/changes-route.d.ts.map +1 -1
- package/dist/vite-plugin/changes-route.js +2 -2
- package/dist/vite-plugin/changes-route.js.map +1 -1
- package/dist/vite-plugin/events-route.d.ts +9 -0
- package/dist/vite-plugin/events-route.d.ts.map +1 -0
- package/dist/vite-plugin/events-route.js +155 -0
- package/dist/vite-plugin/events-route.js.map +1 -0
- package/dist/vite-plugin/index.d.ts +8 -0
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +8 -2
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts +8 -0
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +193 -31
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.js +1 -1
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +12 -12
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { generateConditionalGetRouteHelper } from "../generators/conditional-get.js";
|
|
2
2
|
import { AUTO_GENERATED_ROUTE_HEADER } from "./route-header.js";
|
|
3
3
|
import { generateChangesRoute } from "./changes-route.js";
|
|
4
|
+
import { generateEventsRoute } from "./events-route.js";
|
|
4
5
|
import { generateSyncApplyRoute } from "./sync-apply-route.js";
|
|
5
6
|
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
7
|
import { join, relative } from "node:path";
|
|
@@ -175,6 +176,59 @@ function getApiPublicAccess(apiConfig) {
|
|
|
175
176
|
if (value === true || value === "read") return value;
|
|
176
177
|
return false;
|
|
177
178
|
}
|
|
179
|
+
function toFieldColumnAlias(fieldName) {
|
|
180
|
+
return fieldName.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
|
|
181
|
+
}
|
|
182
|
+
function manifestObjectKey(objectDef) {
|
|
183
|
+
return objectDef.qualifiedName ?? objectDef.className;
|
|
184
|
+
}
|
|
185
|
+
function manifestObjectExtends(objectDef, parent) {
|
|
186
|
+
const extendsQualified = objectDef.extendsQualified;
|
|
187
|
+
const extendsName = objectDef.extends;
|
|
188
|
+
return typeof extendsQualified === "string" && extendsQualified === parent.qualifiedName || typeof extendsName === "string" && (extendsName === parent.className || extendsName === parent.qualifiedName);
|
|
189
|
+
}
|
|
190
|
+
function findManifestParent(objectDef, manifest) {
|
|
191
|
+
return Object.values(manifest.objects).find((candidate) => manifestObjectExtends(objectDef, candidate));
|
|
192
|
+
}
|
|
193
|
+
function collectStiHierarchyObjects(objectDef, manifest) {
|
|
194
|
+
if (!manifest) return [objectDef];
|
|
195
|
+
let cursor = objectDef;
|
|
196
|
+
let stiBase = cursor.decoratorConfig?.tableStrategy === "sti" ? cursor : void 0;
|
|
197
|
+
while (cursor) {
|
|
198
|
+
const parent = findManifestParent(cursor, manifest);
|
|
199
|
+
if (!parent) break;
|
|
200
|
+
if (parent.decoratorConfig?.tableStrategy === "sti") stiBase = parent;
|
|
201
|
+
cursor = parent;
|
|
202
|
+
}
|
|
203
|
+
if (!stiBase) return [objectDef];
|
|
204
|
+
const members = /* @__PURE__ */ new Map();
|
|
205
|
+
const visit = (current) => {
|
|
206
|
+
const key = manifestObjectKey(current);
|
|
207
|
+
if (members.has(key)) return;
|
|
208
|
+
members.set(key, current);
|
|
209
|
+
for (const candidate of Object.values(manifest.objects)) {
|
|
210
|
+
if (candidate === current) continue;
|
|
211
|
+
if (manifestObjectExtends(candidate, current)) visit(candidate);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
visit(stiBase);
|
|
215
|
+
return [...members.values()];
|
|
216
|
+
}
|
|
217
|
+
function collectReadPermissionFields(objectDef, manifest) {
|
|
218
|
+
const result = [];
|
|
219
|
+
const seen = /* @__PURE__ */ new Set();
|
|
220
|
+
for (const member of collectStiHierarchyObjects(objectDef, manifest)) for (const [name, def] of Object.entries(member.fields ?? {})) {
|
|
221
|
+
const permission = typeof def.readPermission === "string" ? def.readPermission : typeof def._meta?.readPermission === "string" ? def._meta.readPermission : void 0;
|
|
222
|
+
if (!permission) continue;
|
|
223
|
+
for (const fieldName of [name, toFieldColumnAlias(name)]) {
|
|
224
|
+
const key = `${fieldName}\0${permission}`;
|
|
225
|
+
if (seen.has(key)) continue;
|
|
226
|
+
seen.add(key);
|
|
227
|
+
result.push([fieldName, permission]);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
178
232
|
/**
|
|
179
233
|
* Emit the fail-closed authorization guard injected into generated route files
|
|
180
234
|
* (#1540, 2c). Every generated CRUD/action handler calls `requireRouteAuth`,
|
|
@@ -182,12 +236,18 @@ function getApiPublicAccess(apiConfig) {
|
|
|
182
236
|
* authenticated principal. Mirrors the knowledge-route `isKnowledgeAdmin`
|
|
183
237
|
* precedent. Mutating verbs require auth even when reads are public.
|
|
184
238
|
*/
|
|
185
|
-
function generateAuthGuardHelper(objectDef) {
|
|
239
|
+
function generateAuthGuardHelper(objectDef, manifest) {
|
|
186
240
|
const publicAccess = getApiPublicAccess(objectDef.decoratorConfig?.api);
|
|
241
|
+
const readPermissionFields = collectReadPermissionFields(objectDef, manifest);
|
|
187
242
|
return `
|
|
188
243
|
// Fail-closed authorization (#1540): generated routes require an authenticated
|
|
189
244
|
// principal on \`locals\` unless explicitly marked \`@smrt({ api: { public } })\`.
|
|
190
245
|
const PUBLIC_ACCESS: boolean | 'read' = ${JSON.stringify(publicAccess)};
|
|
246
|
+
const READ_PERMISSION_FIELDS: Array<[string, string]> = ${JSON.stringify(readPermissionFields)};
|
|
247
|
+
|
|
248
|
+
interface PublicJsonOptions {
|
|
249
|
+
permissions?: Iterable<string>;
|
|
250
|
+
}
|
|
191
251
|
|
|
192
252
|
function hasAuthenticatedPrincipal(locals: unknown): boolean {
|
|
193
253
|
if (!locals || typeof locals !== 'object') return false;
|
|
@@ -215,13 +275,74 @@ function requireRouteAuth(locals: unknown, mutating: boolean): void {
|
|
|
215
275
|
}
|
|
216
276
|
}
|
|
217
277
|
|
|
278
|
+
function getPublicJsonOptions(locals: unknown): PublicJsonOptions {
|
|
279
|
+
const l = readJsonRecord(locals);
|
|
280
|
+
const permissions = l.permissions ?? l.permissionSet ?? l.smrtPermissions;
|
|
281
|
+
if (Array.isArray(permissions) || permissions instanceof Set) {
|
|
282
|
+
return { permissions };
|
|
283
|
+
}
|
|
284
|
+
return {};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function hasReadPermission(
|
|
288
|
+
options: PublicJsonOptions,
|
|
289
|
+
permission: string,
|
|
290
|
+
): boolean {
|
|
291
|
+
if (!options.permissions) return false;
|
|
292
|
+
for (const granted of options.permissions) {
|
|
293
|
+
if (granted === permission) return true;
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function applyReadPermissionRedaction(
|
|
299
|
+
value: unknown,
|
|
300
|
+
options: PublicJsonOptions,
|
|
301
|
+
seen: WeakSet<object> = new WeakSet(),
|
|
302
|
+
): unknown {
|
|
303
|
+
if (value === null || typeof value !== 'object') return value;
|
|
304
|
+
if (seen.has(value)) return null;
|
|
305
|
+
if (hasPublicJson(value)) {
|
|
306
|
+
seen.add(value);
|
|
307
|
+
return applyReadPermissionRedaction(
|
|
308
|
+
value.toPublicJSON(options),
|
|
309
|
+
options,
|
|
310
|
+
seen,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (Array.isArray(value)) {
|
|
314
|
+
seen.add(value);
|
|
315
|
+
return value.map((entry) =>
|
|
316
|
+
applyReadPermissionRedaction(entry, options, seen),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
const proto = Object.getPrototypeOf(value);
|
|
320
|
+
if (proto !== Object.prototype && proto !== null) return value;
|
|
321
|
+
seen.add(value);
|
|
322
|
+
const out = { ...(value as Record<string, unknown>) };
|
|
323
|
+
const metaData =
|
|
324
|
+
out._meta_data && typeof out._meta_data === 'object'
|
|
325
|
+
? { ...(out._meta_data as Record<string, unknown>) }
|
|
326
|
+
: null;
|
|
327
|
+
for (const [fieldName, permission] of READ_PERMISSION_FIELDS) {
|
|
328
|
+
if (hasReadPermission(options, permission)) continue;
|
|
329
|
+
delete out[fieldName];
|
|
330
|
+
if (metaData) delete metaData[fieldName];
|
|
331
|
+
}
|
|
332
|
+
if (metaData) out._meta_data = metaData;
|
|
333
|
+
for (const [key, entry] of Object.entries(out)) {
|
|
334
|
+
out[key] = applyReadPermissionRedaction(entry, options, seen);
|
|
335
|
+
}
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
|
|
218
339
|
// Sensitive-field-safe serialization for custom-action results (#1540): a
|
|
219
340
|
// custom method may return a SmrtObject (or one nested in an array/plain
|
|
220
341
|
// object), so recurse and route each through toPublicJSON() rather than letting
|
|
221
342
|
// JSON.stringify call toJSON(). Non-plain instances (Date, etc.) and primitives
|
|
222
343
|
// pass through; a cycle guard prevents infinite loops.
|
|
223
344
|
interface PublicJsonSource {
|
|
224
|
-
toPublicJSON(): unknown;
|
|
345
|
+
toPublicJSON(options?: PublicJsonOptions): unknown;
|
|
225
346
|
}
|
|
226
347
|
|
|
227
348
|
function hasPublicJson(value: object): value is PublicJsonSource {
|
|
@@ -238,17 +359,18 @@ function readJsonRecord(value: unknown): Record<string, unknown> {
|
|
|
238
359
|
|
|
239
360
|
function toPublicResult(
|
|
240
361
|
value: unknown,
|
|
362
|
+
options: PublicJsonOptions,
|
|
241
363
|
seen: WeakSet<object> = new WeakSet(),
|
|
242
364
|
): unknown {
|
|
243
365
|
if (value === null || typeof value !== 'object') return value;
|
|
244
366
|
if (seen.has(value)) return null;
|
|
245
367
|
if (hasPublicJson(value)) {
|
|
246
368
|
seen.add(value);
|
|
247
|
-
return toPublicResult(value.toPublicJSON(), seen);
|
|
369
|
+
return toPublicResult(value.toPublicJSON(options), options, seen);
|
|
248
370
|
}
|
|
249
371
|
if (Array.isArray(value)) {
|
|
250
372
|
seen.add(value);
|
|
251
|
-
return value.map((entry) => toPublicResult(entry, seen));
|
|
373
|
+
return value.map((entry) => toPublicResult(entry, options, seen));
|
|
252
374
|
}
|
|
253
375
|
const proto = Object.getPrototypeOf(value);
|
|
254
376
|
if (proto !== Object.prototype && proto !== null) return value;
|
|
@@ -257,7 +379,7 @@ function toPublicResult(
|
|
|
257
379
|
for (const [key, entry] of Object.entries(
|
|
258
380
|
value as Record<string, unknown>,
|
|
259
381
|
)) {
|
|
260
|
-
out[key] = toPublicResult(entry, seen);
|
|
382
|
+
out[key] = toPublicResult(entry, options, seen);
|
|
261
383
|
}
|
|
262
384
|
return out;
|
|
263
385
|
}
|
|
@@ -492,12 +614,13 @@ async function generateSvelteKitRoutes(projectRoot, manifest, options) {
|
|
|
492
614
|
}
|
|
493
615
|
continue;
|
|
494
616
|
}
|
|
495
|
-
await generateRoutesForObject(projectRoot, className, objectDef, options);
|
|
617
|
+
await generateRoutesForObject(projectRoot, className, objectDef, manifest, options);
|
|
496
618
|
generatedCount++;
|
|
497
619
|
}
|
|
498
620
|
if (options.knowledge?.api?.enabled) generateKnowledgeRoute(projectRoot, options);
|
|
499
621
|
generateSyncApplyRoute(projectRoot, manifest, options);
|
|
500
622
|
generateChangesRoute(projectRoot, manifest, options);
|
|
623
|
+
generateEventsRoute(projectRoot, manifest, options);
|
|
501
624
|
updateGitignore(projectRoot, options);
|
|
502
625
|
const skippedMsg = skippedCollections > 0 ? ` (skipped ${skippedCollections} collection classes)` : "";
|
|
503
626
|
console.log(`[smrt] Generated routes for ${generatedCount} SMRT objects${skippedMsg}`);
|
|
@@ -780,7 +903,7 @@ export async function getCollection<
|
|
|
780
903
|
/**
|
|
781
904
|
* Generates route files for a single SMRT object
|
|
782
905
|
*/
|
|
783
|
-
async function generateRoutesForObject(projectRoot, className, objectDef, options) {
|
|
906
|
+
async function generateRoutesForObject(projectRoot, className, objectDef, manifest, options) {
|
|
784
907
|
const collectionName = objectDef.collection;
|
|
785
908
|
const routeDir = join(projectRoot, options.routesDir, collectionName);
|
|
786
909
|
const apiConfig = objectDef.decoratorConfig?.api;
|
|
@@ -789,9 +912,9 @@ async function generateRoutesForObject(projectRoot, className, objectDef, option
|
|
|
789
912
|
return;
|
|
790
913
|
}
|
|
791
914
|
const includedActions = resolveStandardCrudActions(apiConfig);
|
|
792
|
-
if (includedActions.includes("list") || includedActions.includes("create")) writeRoute(routeDir, "+server.ts", generateCollectionRouteTemplate(projectRoot, className, objectDef, includedActions, options, routeDir));
|
|
915
|
+
if (includedActions.includes("list") || includedActions.includes("create")) writeRoute(routeDir, "+server.ts", generateCollectionRouteTemplate(projectRoot, className, objectDef, manifest, includedActions, options, routeDir));
|
|
793
916
|
if (includedActions.includes("get") || includedActions.includes("update") || includedActions.includes("delete")) {
|
|
794
|
-
const itemRoute = generateItemRouteTemplate(projectRoot, className, objectDef, includedActions, options, join(routeDir, "[id]"));
|
|
917
|
+
const itemRoute = generateItemRouteTemplate(projectRoot, className, objectDef, manifest, includedActions, options, join(routeDir, "[id]"));
|
|
795
918
|
writeRoute(join(routeDir, "[id]"), "+server.ts", itemRoute);
|
|
796
919
|
}
|
|
797
920
|
const customActions = Object.entries(objectDef.methods).filter(([name, method]) => !STANDARD_API_ACTIONS.includes(name) && method.isPublic && shouldIncludeInApi(name, apiConfig));
|
|
@@ -816,7 +939,7 @@ async function generateRoutesForObject(projectRoot, className, objectDef, option
|
|
|
816
939
|
}
|
|
817
940
|
});
|
|
818
941
|
}
|
|
819
|
-
for (const [actionRouteDir, routeSpecs] of groupCustomActionRoutes(actionSpecs)) writeRoute(actionRouteDir, "+server.ts", generateActionRouteTemplate(projectRoot, actionRouteDir, routeSpecs, objectDef, options));
|
|
942
|
+
for (const [actionRouteDir, routeSpecs] of groupCustomActionRoutes(actionSpecs)) writeRoute(actionRouteDir, "+server.ts", generateActionRouteTemplate(projectRoot, actionRouteDir, routeSpecs, objectDef, manifest, options));
|
|
820
943
|
}
|
|
821
944
|
async function generateCollectionRoutesForObject(projectRoot, className, objectDef, manifest, options) {
|
|
822
945
|
const apiConfig = objectDef.decoratorConfig?.api;
|
|
@@ -851,7 +974,7 @@ async function generateCollectionRoutesForObject(projectRoot, className, objectD
|
|
|
851
974
|
});
|
|
852
975
|
}
|
|
853
976
|
for (const [actionRouteDir, routeSpecs] of groupCustomActionRoutes(actionSpecs)) {
|
|
854
|
-
writeRoute(actionRouteDir, "+server.ts", generateActionRouteTemplate(projectRoot, actionRouteDir, routeSpecs, objectDef, options));
|
|
977
|
+
writeRoute(actionRouteDir, "+server.ts", generateActionRouteTemplate(projectRoot, actionRouteDir, routeSpecs, objectDef, manifest, options));
|
|
855
978
|
generatedAnyRoutes = true;
|
|
856
979
|
}
|
|
857
980
|
return generatedAnyRoutes;
|
|
@@ -998,13 +1121,15 @@ function isLocalObject(projectRoot, objectDef) {
|
|
|
998
1121
|
/**
|
|
999
1122
|
* Generates collection route template (GET list, POST create)
|
|
1000
1123
|
*/
|
|
1001
|
-
function generateCollectionRouteTemplate(projectRoot, className, objectDef, includedActions, options, routeDir) {
|
|
1124
|
+
function generateCollectionRouteTemplate(projectRoot, className, objectDef, manifest, includedActions, options, routeDir) {
|
|
1002
1125
|
const hasGet = includedActions.includes("list");
|
|
1003
1126
|
const hasPost = includedActions.includes("create");
|
|
1004
1127
|
const modelType = resolveObjectTypeReference(projectRoot, className, objectDef, options, routeDir);
|
|
1005
1128
|
const serializers = resolveStandardRouteSerializers(objectDef.decoratorConfig?.api);
|
|
1006
1129
|
const serializerImports = serializers.importStatements.join("\n");
|
|
1007
1130
|
const listUsesSerializer = !!serializers.listItemSerializerName;
|
|
1131
|
+
const listUsesPermissionScopedBody = collectReadPermissionFields(objectDef, manifest).length > 0;
|
|
1132
|
+
const listUsesBodyHash = listUsesSerializer || listUsesPermissionScopedBody;
|
|
1008
1133
|
const imports = `${AUTO_GENERATED_ROUTE_HEADER}
|
|
1009
1134
|
// DO NOT EDIT - changes will be overwritten
|
|
1010
1135
|
|
|
@@ -1012,10 +1137,11 @@ import { error${hasPost ? ", json" : ""} } from '@sveltejs/kit';
|
|
|
1012
1137
|
${serializerImports ? `${serializerImports}\n` : ""}import { getCollection } from '$lib/server/smrt';
|
|
1013
1138
|
${modelType.importStatement ? `${modelType.importStatement}\n` : ""}import type { RequestHandler } from './$types';
|
|
1014
1139
|
// Note: ${className} is auto-registered by the Vite plugin scanner
|
|
1015
|
-
${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPost ? generateWritablePolicyHelper(objectDef) : ""}${hasGet ? generateConditionalGetRouteHelper(objectDef.decoratorConfig?.api, {
|
|
1140
|
+
${generateAuthGuardHelper(objectDef, manifest)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPost ? generateWritablePolicyHelper(objectDef) : ""}${hasGet ? generateConditionalGetRouteHelper(objectDef.decoratorConfig?.api, {
|
|
1016
1141
|
tenantScoped: isTenantScoped(objectDef),
|
|
1142
|
+
permissionScoped: listUsesPermissionScopedBody,
|
|
1017
1143
|
modelName: className,
|
|
1018
|
-
useBodyHash:
|
|
1144
|
+
useBodyHash: listUsesBodyHash
|
|
1019
1145
|
}) : ""}`;
|
|
1020
1146
|
const listAndCount = isTenantScoped(objectDef) ? ` const readScope = tenantReadScope();
|
|
1021
1147
|
const items = await collection.list({ limit, offset, where: readScope });
|
|
@@ -1025,6 +1151,7 @@ ${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenan
|
|
|
1025
1151
|
// List all ${className.toLowerCase()}s
|
|
1026
1152
|
export const GET: RequestHandler = async ({ locals, url, request }) => {
|
|
1027
1153
|
${routeGuardPreamble(objectDef, false)}
|
|
1154
|
+
const publicJsonOptions = getPublicJsonOptions(locals);
|
|
1028
1155
|
const limit = Number(url.searchParams.get('limit')) || 50;
|
|
1029
1156
|
const offset = Number(url.searchParams.get('offset')) || 0;
|
|
1030
1157
|
|
|
@@ -1032,13 +1159,21 @@ ${generateCollectionLoad(className, { typeName: modelType.typeName })}
|
|
|
1032
1159
|
${listUsesSerializer ? `${listAndCount}
|
|
1033
1160
|
// Custom serializer may render related-table data → v1 body-hash ETag (#1765).
|
|
1034
1161
|
const serializedItems = await Promise.all(
|
|
1035
|
-
items.map((item) =>
|
|
1162
|
+
items.map(async (item) =>
|
|
1163
|
+
applyReadPermissionRedaction(
|
|
1164
|
+
await ${serializers.listItemSerializerName}(item),
|
|
1165
|
+
publicJsonOptions,
|
|
1166
|
+
),
|
|
1167
|
+
),
|
|
1036
1168
|
);
|
|
1037
|
-
return conditionalJson(request, { items: serializedItems, count, limit, offset });` :
|
|
1169
|
+
return conditionalJson(request, { items: serializedItems, count, limit, offset });` : listUsesPermissionScopedBody ? `${listAndCount}
|
|
1170
|
+
// Field read permissions make the body vary by caller permissions → v1 body-hash ETag (#1565).
|
|
1171
|
+
const items_public = items.map((item) => item.toPublicJSON(publicJsonOptions));
|
|
1172
|
+
return conditionalJson(request, { items: items_public, count, limit, offset });` : ` // ETag v2 (#1765): the table-version ETag is checked first; on a concrete
|
|
1038
1173
|
// If-None-Match the list query below never runs (zero-query 304).
|
|
1039
1174
|
return conditionalVersionedRead(request, collection.db, collection.tableName, async () => {
|
|
1040
1175
|
${listAndCount}
|
|
1041
|
-
const items_public = items.map((item) => item.toPublicJSON());
|
|
1176
|
+
const items_public = items.map((item) => item.toPublicJSON(publicJsonOptions));
|
|
1042
1177
|
return { items: items_public, count, limit, offset };
|
|
1043
1178
|
});`}
|
|
1044
1179
|
};
|
|
@@ -1047,6 +1182,7 @@ ${listAndCount}
|
|
|
1047
1182
|
// Create new ${className.toLowerCase()}
|
|
1048
1183
|
export const POST: RequestHandler = async ({ locals, request }) => {
|
|
1049
1184
|
${routeGuardPreamble(objectDef, true)}
|
|
1185
|
+
const publicJsonOptions = getPublicJsonOptions(locals);
|
|
1050
1186
|
const body: unknown = await request.json();
|
|
1051
1187
|
const data = applyWritablePolicy(body);
|
|
1052
1188
|
|
|
@@ -1056,8 +1192,11 @@ ${generateCollectionLoad(className, { typeName: modelType.typeName })}
|
|
|
1056
1192
|
${serializers.itemSerializerName ? `
|
|
1057
1193
|
const serializedItem = await ${serializers.itemSerializerName}(item);
|
|
1058
1194
|
|
|
1059
|
-
return json(
|
|
1060
|
-
|
|
1195
|
+
return json(
|
|
1196
|
+
applyReadPermissionRedaction(serializedItem, publicJsonOptions),
|
|
1197
|
+
{ status: 201 },
|
|
1198
|
+
);` : `
|
|
1199
|
+
return json(item.toPublicJSON(publicJsonOptions), { status: 201 });`}
|
|
1061
1200
|
};
|
|
1062
1201
|
` : "";
|
|
1063
1202
|
return imports + getHandler + postHandler;
|
|
@@ -1101,7 +1240,7 @@ function generateClassNotRegisteredError(className) {
|
|
|
1101
1240
|
/**
|
|
1102
1241
|
* Generates item route template (GET, PUT, DELETE)
|
|
1103
1242
|
*/
|
|
1104
|
-
function generateItemRouteTemplate(projectRoot, className, objectDef, includedActions, options, routeDir) {
|
|
1243
|
+
function generateItemRouteTemplate(projectRoot, className, objectDef, manifest, includedActions, options, routeDir) {
|
|
1105
1244
|
const hasGet = includedActions.includes("get");
|
|
1106
1245
|
const hasPut = includedActions.includes("update");
|
|
1107
1246
|
const hasDelete = includedActions.includes("delete");
|
|
@@ -1110,16 +1249,19 @@ function generateItemRouteTemplate(projectRoot, className, objectDef, includedAc
|
|
|
1110
1249
|
const serializers = resolveStandardRouteSerializers(objectDef.decoratorConfig?.api);
|
|
1111
1250
|
const serializerImports = serializers.importStatements.join("\n");
|
|
1112
1251
|
const getUsesSerializer = !!serializers.itemSerializerName;
|
|
1252
|
+
const getUsesPermissionScopedBody = collectReadPermissionFields(objectDef, manifest).length > 0;
|
|
1253
|
+
const getUsesBodyHash = getUsesSerializer || getUsesPermissionScopedBody;
|
|
1113
1254
|
const imports = `${AUTO_GENERATED_ROUTE_HEADER}
|
|
1114
1255
|
// DO NOT EDIT - changes will be overwritten
|
|
1115
1256
|
|
|
1116
1257
|
import { error${hasPut || hasDelete ? ", json" : ""} } from '@sveltejs/kit';
|
|
1117
1258
|
${serializerImports ? `${serializerImports}\n` : ""}import { getCollection } from '$lib/server/smrt';
|
|
1118
1259
|
${modelType.importStatement ? `${modelType.importStatement}\n` : ""}import type { RequestHandler } from './$types';
|
|
1119
|
-
${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPut ? generateWritablePolicyHelper(objectDef) : ""}${hasGet ? generateConditionalGetRouteHelper(objectDef.decoratorConfig?.api, {
|
|
1260
|
+
${generateAuthGuardHelper(objectDef, manifest)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPut ? generateWritablePolicyHelper(objectDef) : ""}${hasGet ? generateConditionalGetRouteHelper(objectDef.decoratorConfig?.api, {
|
|
1120
1261
|
tenantScoped: isTenantScoped(objectDef),
|
|
1262
|
+
permissionScoped: getUsesPermissionScopedBody,
|
|
1121
1263
|
modelName: className,
|
|
1122
|
-
useBodyHash:
|
|
1264
|
+
useBodyHash: getUsesBodyHash
|
|
1123
1265
|
}) : ""}`;
|
|
1124
1266
|
const getForRead = isTenantScoped(objectDef) ? ` const readScope = tenantReadScope();
|
|
1125
1267
|
const item = await collection.get(
|
|
@@ -1129,18 +1271,25 @@ ${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenan
|
|
|
1129
1271
|
// Get single ${simpleClassName.toLowerCase()}
|
|
1130
1272
|
export const GET: RequestHandler = async ({ locals, params, request }) => {
|
|
1131
1273
|
${routeGuardPreamble(objectDef, false)}
|
|
1274
|
+
const publicJsonOptions = getPublicJsonOptions(locals);
|
|
1132
1275
|
${generateCollectionLoad(className, { typeName: modelType.typeName })}
|
|
1133
1276
|
${getUsesSerializer ? `${getForRead}
|
|
1134
1277
|
${generateNotFoundError(className)}
|
|
1135
1278
|
// Custom serializer may render related-table data → v1 body-hash ETag (#1765).
|
|
1136
1279
|
const serializedItem = await ${serializers.itemSerializerName}(item);
|
|
1137
|
-
return conditionalJson(
|
|
1280
|
+
return conditionalJson(
|
|
1281
|
+
request,
|
|
1282
|
+
applyReadPermissionRedaction(serializedItem, publicJsonOptions),
|
|
1283
|
+
);` : getUsesPermissionScopedBody ? `${getForRead}
|
|
1284
|
+
${generateNotFoundError(className)}
|
|
1285
|
+
// Field read permissions make the body vary by caller permissions → v1 body-hash ETag (#1565).
|
|
1286
|
+
return conditionalJson(request, item.toPublicJSON(publicJsonOptions));` : ` // ETag v2 (#1765): a concrete If-None-Match answers 304 without the row fetch
|
|
1138
1287
|
// (a delete advances the version, so a since-deleted row can't false-match);
|
|
1139
1288
|
// a wildcard \`*\` is deferred until the fetch confirms the row exists.
|
|
1140
1289
|
return conditionalVersionedRead(request, collection.db, collection.tableName, async () => {
|
|
1141
1290
|
${getForRead}
|
|
1142
1291
|
${generateNotFoundError(className)}
|
|
1143
|
-
return item.toPublicJSON();
|
|
1292
|
+
return item.toPublicJSON(publicJsonOptions);
|
|
1144
1293
|
});`}
|
|
1145
1294
|
};
|
|
1146
1295
|
` : "";
|
|
@@ -1148,6 +1297,7 @@ ${generateNotFoundError(className)}
|
|
|
1148
1297
|
// Update ${simpleClassName.toLowerCase()}
|
|
1149
1298
|
export const PUT: RequestHandler = async ({ locals, params, request }) => {
|
|
1150
1299
|
${routeGuardPreamble(objectDef, true)}
|
|
1300
|
+
const publicJsonOptions = getPublicJsonOptions(locals);
|
|
1151
1301
|
${generateCollectionLoad(className, { typeName: modelType.typeName })}
|
|
1152
1302
|
const item = await collection.get(params.id);
|
|
1153
1303
|
${generateNotFoundError(className)}
|
|
@@ -1159,8 +1309,8 @@ ${generateNotFoundError(className)}
|
|
|
1159
1309
|
${serializers.itemSerializerName ? `
|
|
1160
1310
|
const serializedItem = await ${serializers.itemSerializerName}(item);
|
|
1161
1311
|
|
|
1162
|
-
return json(serializedItem);` : `
|
|
1163
|
-
return json(item.toPublicJSON());`}
|
|
1312
|
+
return json(applyReadPermissionRedaction(serializedItem, publicJsonOptions));` : `
|
|
1313
|
+
return json(item.toPublicJSON(publicJsonOptions));`}
|
|
1164
1314
|
};
|
|
1165
1315
|
` : "";
|
|
1166
1316
|
const deleteHandler = hasDelete ? `
|
|
@@ -1180,7 +1330,7 @@ ${generateNotFoundError(className)}
|
|
|
1180
1330
|
/**
|
|
1181
1331
|
* Generates custom action route template
|
|
1182
1332
|
*/
|
|
1183
|
-
function generateActionRouteTemplate(projectRoot, routeDir, routeSpecs, objectDef, options) {
|
|
1333
|
+
function generateActionRouteTemplate(projectRoot, routeDir, routeSpecs, objectDef, manifest, options) {
|
|
1184
1334
|
if (routeSpecs.length === 0) throw new Error("Cannot generate a custom action route without handlers");
|
|
1185
1335
|
const [firstSpec] = routeSpecs;
|
|
1186
1336
|
const { lookupClassName, routeConfig, hostType } = firstSpec;
|
|
@@ -1200,7 +1350,7 @@ function generateActionRouteTemplate(projectRoot, routeDir, routeSpecs, objectDe
|
|
|
1200
1350
|
// DO NOT EDIT - changes will be overwritten
|
|
1201
1351
|
|
|
1202
1352
|
${importBlock}
|
|
1203
|
-
${generateAuthGuardHelper(objectDef)}${tenantScoped ? generateTenantContextHelper() : ""}
|
|
1353
|
+
${generateAuthGuardHelper(objectDef, manifest)}${tenantScoped ? generateTenantContextHelper() : ""}
|
|
1204
1354
|
${handlers}`;
|
|
1205
1355
|
}
|
|
1206
1356
|
function generateActionRouteHandler(lookupClassName, actionName, actionDef, routeConfig, hostType, tenantScoped, lookupTypeName, hostTypeName) {
|
|
@@ -1219,6 +1369,7 @@ function generateActionRouteHandler(lookupClassName, actionName, actionDef, rout
|
|
|
1219
1369
|
return `// Custom collection method: ${actionName}
|
|
1220
1370
|
export const ${handlerName}: RequestHandler = async (${collectionHandlerArgs}) => {
|
|
1221
1371
|
${authGuardLine}
|
|
1372
|
+
const publicJsonOptions = getPublicJsonOptions(locals);
|
|
1222
1373
|
${generateCollectionLoad(lookupClassName, { typeName: lookupTypeName })}
|
|
1223
1374
|
const typedCollection = collection as unknown as ${hostTypeName};
|
|
1224
1375
|
${generateCollectionNotRegisteredError(lookupClassName)}
|
|
@@ -1226,7 +1377,10 @@ ${staticTargetLoad}
|
|
|
1226
1377
|
|
|
1227
1378
|
${optionsLoad} const result = ${buildActionInvocationExpression(receiverExpression, actionName, invocationArgs)};
|
|
1228
1379
|
|
|
1229
|
-
return json({
|
|
1380
|
+
return json({
|
|
1381
|
+
action: '${actionName}',
|
|
1382
|
+
result: toPublicResult(result, publicJsonOptions),
|
|
1383
|
+
});
|
|
1230
1384
|
};
|
|
1231
1385
|
`;
|
|
1232
1386
|
}
|
|
@@ -1235,13 +1389,17 @@ ${optionsLoad} const result = ${buildActionInvocationExpression(receiverExpress
|
|
|
1235
1389
|
return `// Custom collection action: ${actionName}
|
|
1236
1390
|
export const ${handlerName}: RequestHandler = async (${collectionHandlerArgs}) => {
|
|
1237
1391
|
${authGuardLine}
|
|
1392
|
+
const publicJsonOptions = getPublicJsonOptions(locals);
|
|
1238
1393
|
const registered = ObjectRegistry.getClass('${lookupClassName}');
|
|
1239
1394
|
${generateClassNotRegisteredError(lookupClassName)}
|
|
1240
1395
|
|
|
1241
1396
|
${optionsLoad} const ClassRef = registered.constructor as typeof ${hostTypeName};
|
|
1242
1397
|
const result = ${buildActionInvocationExpression("ClassRef", actionName, invocationArgs)};
|
|
1243
1398
|
|
|
1244
|
-
return json({
|
|
1399
|
+
return json({
|
|
1400
|
+
action: '${actionName}',
|
|
1401
|
+
result: toPublicResult(result, publicJsonOptions),
|
|
1402
|
+
});
|
|
1245
1403
|
};
|
|
1246
1404
|
`;
|
|
1247
1405
|
}
|
|
@@ -1249,13 +1407,17 @@ ${optionsLoad} const ClassRef = registered.constructor as typeof ${hostTypeName
|
|
|
1249
1407
|
return `// Custom action: ${actionName}
|
|
1250
1408
|
export const ${handlerName}: RequestHandler = async (${itemHandlerArgs}) => {
|
|
1251
1409
|
${authGuardLine}
|
|
1410
|
+
const publicJsonOptions = getPublicJsonOptions(locals);
|
|
1252
1411
|
${generateCollectionLoad(lookupClassName, { typeName: lookupTypeName })}
|
|
1253
1412
|
const item = await collection.get(params.id);
|
|
1254
1413
|
${generateNotFoundError(lookupClassName)}
|
|
1255
1414
|
|
|
1256
1415
|
${optionsLoad} const result = ${buildActionInvocationExpression("item", actionName, invocationArgs)};
|
|
1257
1416
|
|
|
1258
|
-
return json({
|
|
1417
|
+
return json({
|
|
1418
|
+
action: '${actionName}',
|
|
1419
|
+
result: toPublicResult(result, publicJsonOptions),
|
|
1420
|
+
});
|
|
1259
1421
|
};
|
|
1260
1422
|
`;
|
|
1261
1423
|
}
|