@happyvertical/smrt-core 0.36.0 → 0.36.2

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.
@@ -62,6 +62,53 @@ function resolveStandardRouteSerializers(apiConfig) {
62
62
  listItemSerializerName
63
63
  };
64
64
  }
65
+ function isValidTypeIdentifier(name) {
66
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
67
+ }
68
+ function resolveObjectTypeReference(projectRoot, className, objectDef, options, routeDir) {
69
+ const simpleClassName = extractSimpleClassName(className);
70
+ if (!objectDef || !isValidTypeIdentifier(simpleClassName)) {
71
+ return { typeName: "import('@happyvertical/smrt-core').SmrtObject" };
72
+ }
73
+ const importPath = isLocalObject(projectRoot, objectDef) || !objectDef.packageName ? getRouteTypeImportPath(
74
+ projectRoot,
75
+ objectDef,
76
+ options,
77
+ simpleClassName,
78
+ routeDir
79
+ ) : objectDef.packageName;
80
+ return {
81
+ typeName: simpleClassName,
82
+ importStatement: `import type { ${simpleClassName} } from '${importPath}';`
83
+ };
84
+ }
85
+ function getRouteTypeImportPath(projectRoot, objectDef, options, simpleClassName, routeDir) {
86
+ if (objectDef.filePath) {
87
+ const srcLibDir = join(projectRoot, "src/lib");
88
+ if (objectDef.filePath.startsWith(`${srcLibDir}/`)) {
89
+ const libRelative = relative(srcLibDir, objectDef.filePath).replace(/\\/g, "/").replace(/\.(ts|js|tsx|jsx)$/, "");
90
+ return `$lib/${libRelative}`.replace(/\/+/g, "/");
91
+ }
92
+ if (routeDir) {
93
+ const routeRelative = relative(routeDir, objectDef.filePath).replace(/\\/g, "/").replace(/\.(ts|js|tsx|jsx)$/, "");
94
+ return routeRelative.startsWith(".") ? routeRelative : `./${routeRelative}`;
95
+ }
96
+ }
97
+ return getSvelteKitImportPath(
98
+ projectRoot,
99
+ objectDef.filePath,
100
+ options.objectsDir,
101
+ simpleClassName
102
+ );
103
+ }
104
+ function mergeImportStatements(importStatements) {
105
+ return Array.from(new Set(importStatements.filter(Boolean))).join("\n");
106
+ }
107
+ function findObjectDefByRegistryKey(manifest, className) {
108
+ return manifest.objects[className] ?? Object.values(manifest.objects).find(
109
+ (objectDef) => objectDef.className === extractSimpleClassName(className)
110
+ );
111
+ }
65
112
  function collectReadonlyFieldNames(objectDef) {
66
113
  const fields = objectDef.fields || {};
67
114
  const names = [];
@@ -155,20 +202,43 @@ function requireRouteAuth(locals: unknown, mutating: boolean): void {
155
202
  // object), so recurse and route each through toPublicJSON() rather than letting
156
203
  // JSON.stringify call toJSON(). Non-plain instances (Date, etc.) and primitives
157
204
  // pass through; a cycle guard prevents infinite loops.
158
- function toPublicResult(value: any, seen: WeakSet<object> = new WeakSet()): any {
205
+ interface PublicJsonSource {
206
+ toPublicJSON(): unknown;
207
+ }
208
+
209
+ function hasPublicJson(value: object): value is PublicJsonSource {
210
+ return (
211
+ 'toPublicJSON' in value &&
212
+ typeof (value as { toPublicJSON?: unknown }).toPublicJSON === 'function'
213
+ );
214
+ }
215
+
216
+ function readJsonRecord(value: unknown): Record<string, unknown> {
217
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
218
+ return value as Record<string, unknown>;
219
+ }
220
+
221
+ function toPublicResult(
222
+ value: unknown,
223
+ seen: WeakSet<object> = new WeakSet(),
224
+ ): unknown {
159
225
  if (value === null || typeof value !== 'object') return value;
160
- if (typeof value.toPublicJSON === 'function') return value.toPublicJSON();
226
+ if (seen.has(value)) return null;
227
+ if (hasPublicJson(value)) {
228
+ seen.add(value);
229
+ return toPublicResult(value.toPublicJSON(), seen);
230
+ }
161
231
  if (Array.isArray(value)) {
162
- if (seen.has(value)) return value;
163
232
  seen.add(value);
164
- return value.map((entry: any) => toPublicResult(entry, seen));
233
+ return value.map((entry) => toPublicResult(entry, seen));
165
234
  }
166
235
  const proto = Object.getPrototypeOf(value);
167
236
  if (proto !== Object.prototype && proto !== null) return value;
168
- if (seen.has(value)) return value;
169
237
  seen.add(value);
170
- const out: Record<string, any> = {};
171
- for (const [key, entry] of Object.entries(value)) {
238
+ const out: Record<string, unknown> = {};
239
+ for (const [key, entry] of Object.entries(
240
+ value as Record<string, unknown>,
241
+ )) {
172
242
  out[key] = toPublicResult(entry, seen);
173
243
  }
174
244
  return out;
@@ -185,8 +255,10 @@ import { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenanc
185
255
  function establishTenantContext(locals: unknown): void {
186
256
  if (hasTenantContext()) return;
187
257
  if (!locals || typeof locals !== 'object') return;
188
- const l = locals as Record<string, any>;
189
- const tenantId = l.tenantId ?? l.user?.tenantId ?? l.session?.tenantId;
258
+ const l = locals as Record<string, unknown>;
259
+ const user = l.user as Record<string, unknown> | undefined;
260
+ const session = l.session as Record<string, unknown> | undefined;
261
+ const tenantId = l.tenantId ?? user?.tenantId ?? session?.tenantId;
190
262
  if (typeof tenantId === 'string' && tenantId) {
191
263
  enterTenantContext({ tenantId });
192
264
  }
@@ -275,6 +347,92 @@ function buildActionInvocationArgs(actionDef) {
275
347
  (parameter) => buildOptionsPropertyAccess(parameter.name)
276
348
  );
277
349
  }
350
+ function buildObjectTypeProperty(propertyName) {
351
+ if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(propertyName)) {
352
+ return propertyName;
353
+ }
354
+ return JSON.stringify(propertyName);
355
+ }
356
+ function buildActionTargetTypeExpression(targetTypeName, isStatic) {
357
+ return isStatic ? `(typeof ${targetTypeName})` : targetTypeName;
358
+ }
359
+ function buildActionArgsTypeAlias(actionName, targetTypeName, isStatic) {
360
+ const targetTypeExpression = buildActionTargetTypeExpression(
361
+ targetTypeName,
362
+ isStatic
363
+ );
364
+ return ` type ActionArgs = Parameters<${targetTypeExpression}[${JSON.stringify(actionName)}]>;`;
365
+ }
366
+ function buildActionOptionsTypeAlias(actionDef) {
367
+ const parameters = Array.isArray(actionDef.parameters) ? actionDef.parameters : [];
368
+ if (parameters.length <= 1 && parameters[0]?.name === "options") {
369
+ return "";
370
+ }
371
+ return [
372
+ " type ActionOptions = {",
373
+ ...parameters.map(
374
+ (parameter, index) => ` ${buildObjectTypeProperty(parameter.name)}: ActionArgs[${index}];`
375
+ ),
376
+ " };"
377
+ ].join("\n");
378
+ }
379
+ function buildActionOptionsLoad(actionName, actionDef, routeConfig, targetTypeName, isStatic) {
380
+ const parameters = Array.isArray(actionDef.parameters) ? actionDef.parameters : [];
381
+ if (parameters.length === 0) {
382
+ return "";
383
+ }
384
+ const hasPathParams = routeConfig.pathParamNames.length > 0;
385
+ const pathParamsObjectLiteral = buildPathParamsObjectLiteral(
386
+ routeConfig.pathParamNames
387
+ );
388
+ const isSingleOptionsParameter = parameters.length === 1 && parameters[0]?.name === "options";
389
+ const lines = [
390
+ buildActionArgsTypeAlias(actionName, targetTypeName, isStatic)
391
+ ];
392
+ const optionsTypeAlias = buildActionOptionsTypeAlias(actionDef);
393
+ if (optionsTypeAlias) {
394
+ lines.push(optionsTypeAlias);
395
+ }
396
+ if (routeConfig.method === "GET") {
397
+ if (hasPathParams) {
398
+ lines.push(
399
+ ` const pathParams = ${pathParamsObjectLiteral};`,
400
+ " const options = {",
401
+ " ...Object.fromEntries(new URL(request.url).searchParams.entries()),",
402
+ " ...pathParams,",
403
+ ` } as ${isSingleOptionsParameter ? "ActionArgs[0]" : "ActionOptions"};`,
404
+ ""
405
+ );
406
+ } else {
407
+ lines.push(
408
+ " const options = Object.fromEntries(",
409
+ " new URL(request.url).searchParams.entries(),",
410
+ ` ) as ${isSingleOptionsParameter ? "ActionArgs[0]" : "ActionOptions"};`,
411
+ ""
412
+ );
413
+ }
414
+ return lines.join("\n");
415
+ }
416
+ if (hasPathParams) {
417
+ lines.push(
418
+ ` const pathParams = ${pathParamsObjectLiteral};`,
419
+ " const body: unknown = await request.json();",
420
+ " const options = {",
421
+ " ...readJsonRecord(body),",
422
+ " ...pathParams,",
423
+ ` } as ${isSingleOptionsParameter ? "ActionArgs[0]" : "ActionOptions"};`,
424
+ ""
425
+ );
426
+ return lines.join("\n");
427
+ }
428
+ lines.push(" const body: unknown = await request.json();");
429
+ if (isSingleOptionsParameter) {
430
+ lines.push(" const options = body as ActionArgs[0];", "");
431
+ } else {
432
+ lines.push(" const options = readJsonRecord(body) as ActionOptions;", "");
433
+ }
434
+ return lines.join("\n");
435
+ }
278
436
  function buildOptionsPropertyAccess(propertyName) {
279
437
  if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(propertyName)) {
280
438
  return `options.${propertyName}`;
@@ -380,7 +538,9 @@ function clearGeneratedRouteFiles(routesRoot) {
380
538
  }
381
539
  }
382
540
  function knowledgeRouteDir(projectRoot, options) {
383
- const basePath = options.knowledge?.api?.basePath || "/__smrt/knowledge";
541
+ const basePath = String(
542
+ options.knowledge?.api?.basePath || "/__smrt/knowledge"
543
+ );
384
544
  const routeRoot = svelteKitRouteRoot(options.routesDir);
385
545
  const segments = basePath.split("/").map((segment) => segment.trim()).filter(Boolean);
386
546
  return join(projectRoot, routeRoot, ...segments);
@@ -710,7 +870,9 @@ async function generateRoutesForObject(projectRoot, className, objectDef, option
710
870
  projectRoot,
711
871
  className,
712
872
  objectDef,
713
- includedActions
873
+ includedActions,
874
+ options,
875
+ routeDir
714
876
  );
715
877
  writeRoute(routeDir, "+server.ts", collectionRoute);
716
878
  }
@@ -719,7 +881,9 @@ async function generateRoutesForObject(projectRoot, className, objectDef, option
719
881
  projectRoot,
720
882
  className,
721
883
  objectDef,
722
- includedActions
884
+ includedActions,
885
+ options,
886
+ join(routeDir, "[id]")
723
887
  );
724
888
  writeRoute(join(routeDir, "[id]"), "+server.ts", itemRoute);
725
889
  }
@@ -745,6 +909,8 @@ async function generateRoutesForObject(projectRoot, className, objectDef, option
745
909
  routeDir: join(actionBaseDir, ...routeConfig.pathSegments),
746
910
  spec: {
747
911
  lookupClassName: className,
912
+ lookupObjectDef: objectDef,
913
+ hostClassName: className,
748
914
  actionName,
749
915
  actionDef,
750
916
  routeConfig,
@@ -757,8 +923,10 @@ async function generateRoutesForObject(projectRoot, className, objectDef, option
757
923
  )) {
758
924
  const actionRoute = generateActionRouteTemplate(
759
925
  projectRoot,
926
+ actionRouteDir,
760
927
  routeSpecs,
761
- objectDef
928
+ objectDef,
929
+ options
762
930
  );
763
931
  writeRoute(actionRouteDir, "+server.ts", actionRoute);
764
932
  }
@@ -775,6 +943,7 @@ async function generateCollectionRoutesForObject(projectRoot, className, objectD
775
943
  objectDef,
776
944
  manifest
777
945
  );
946
+ const lookupObjectDef = findObjectDefByRegistryKey(manifest, lookupClassName);
778
947
  const customActions = Object.entries(objectDef.methods).filter(
779
948
  ([name, method]) => !STANDARD_API_ACTIONS.includes(name) && method.isPublic && shouldIncludeInApi(name, apiConfig)
780
949
  );
@@ -801,6 +970,8 @@ async function generateCollectionRoutesForObject(projectRoot, className, objectD
801
970
  routeDir: join(routeDir, ...routeConfig.pathSegments),
802
971
  spec: {
803
972
  lookupClassName,
973
+ lookupObjectDef,
974
+ hostClassName: className,
804
975
  actionName,
805
976
  actionDef,
806
977
  routeConfig,
@@ -813,8 +984,10 @@ async function generateCollectionRoutesForObject(projectRoot, className, objectD
813
984
  )) {
814
985
  const actionRoute = generateActionRouteTemplate(
815
986
  projectRoot,
987
+ actionRouteDir,
816
988
  routeSpecs,
817
- objectDef
989
+ objectDef,
990
+ options
818
991
  );
819
992
  writeRoute(actionRouteDir, "+server.ts", actionRoute);
820
993
  generatedAnyRoutes = true;
@@ -922,7 +1095,7 @@ function writeRoute(dir, filename, content) {
922
1095
  }
923
1096
  }
924
1097
  function getSvelteKitImportPath(projectRoot, objectFilePath, objectsDir, className) {
925
- const filePath = join(projectRoot, objectsDir, `${className || "Object"}.ts`);
1098
+ const filePath = objectFilePath || join(projectRoot, objectsDir, `${className || "Object"}.ts`);
926
1099
  const absoluteObjectsDir = objectsDir.startsWith("/") ? objectsDir : join(projectRoot, objectsDir);
927
1100
  const relativePath = relative(absoluteObjectsDir, filePath);
928
1101
  const normalizedPath = relativePath.replace(/\\/g, "/");
@@ -938,9 +1111,16 @@ function isLocalObject(projectRoot, objectDef) {
938
1111
  if (!objectDef.filePath) return false;
939
1112
  return objectDef.filePath.startsWith(projectRoot) && !objectDef.filePath.includes("/node_modules/");
940
1113
  }
941
- function generateCollectionRouteTemplate(_projectRoot, className, objectDef, includedActions, _options) {
1114
+ function generateCollectionRouteTemplate(projectRoot, className, objectDef, includedActions, options, routeDir) {
942
1115
  const hasGet = includedActions.includes("list");
943
1116
  const hasPost = includedActions.includes("create");
1117
+ const modelType = resolveObjectTypeReference(
1118
+ projectRoot,
1119
+ className,
1120
+ objectDef,
1121
+ options,
1122
+ routeDir
1123
+ );
944
1124
  const serializers = resolveStandardRouteSerializers(
945
1125
  objectDef.decoratorConfig?.api
946
1126
  );
@@ -951,7 +1131,8 @@ function generateCollectionRouteTemplate(_projectRoot, className, objectDef, inc
951
1131
  import { error, json } from '@sveltejs/kit';
952
1132
  ${serializerImports ? `${serializerImports}
953
1133
  ` : ""}import { getCollection } from '$lib/server/smrt';
954
- import type { RequestHandler } from './$types';
1134
+ ${modelType.importStatement ? `${modelType.importStatement}
1135
+ ` : ""}import type { RequestHandler } from './$types';
955
1136
  // Note: ${className} is auto-registered by the Vite plugin scanner
956
1137
  ${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPost ? generateWritablePolicyHelper(objectDef) : ""}`;
957
1138
  const getHandler = hasGet ? `
@@ -961,7 +1142,7 @@ ${routeGuardPreamble(objectDef, false)}
961
1142
  const limit = Number(url.searchParams.get('limit')) || 50;
962
1143
  const offset = Number(url.searchParams.get('offset')) || 0;
963
1144
 
964
- ${generateCollectionLoad(className)}
1145
+ ${generateCollectionLoad(className, { typeName: modelType.typeName })}
965
1146
  const items = await collection.list({ limit, offset });
966
1147
  const count = await collection.count();
967
1148
  ${serializers.listItemSerializerName ? `
@@ -978,9 +1159,10 @@ ${serializers.listItemSerializerName ? `
978
1159
  // Create new ${className.toLowerCase()}
979
1160
  export const POST: RequestHandler = async ({ locals, request }) => {
980
1161
  ${routeGuardPreamble(objectDef, true)}
981
- const data = applyWritablePolicy(await request.json());
1162
+ const body: unknown = await request.json();
1163
+ const data = applyWritablePolicy(body);
982
1164
 
983
- ${generateCollectionLoad(className)}
1165
+ ${generateCollectionLoad(className, { typeName: modelType.typeName })}
984
1166
  const item = await collection.create(data);
985
1167
  await item.save();
986
1168
  ${serializers.itemSerializerName ? `
@@ -993,7 +1175,7 @@ ${serializers.itemSerializerName ? `
993
1175
  return imports + getHandler + postHandler;
994
1176
  }
995
1177
  function generateCollectionLoad(className, options = {}) {
996
- const genericSuffix = options.generic ? "<any>" : "";
1178
+ const genericSuffix = options.typeName ? `<${options.typeName}>` : "";
997
1179
  return [
998
1180
  ` const collection = await getCollection${genericSuffix}(`,
999
1181
  ` '${className}',`,
@@ -1031,11 +1213,18 @@ function generateClassNotRegisteredError(className) {
1031
1213
  " );"
1032
1214
  ].join("\n");
1033
1215
  }
1034
- function generateItemRouteTemplate(_projectRoot, className, objectDef, includedActions, _options) {
1216
+ function generateItemRouteTemplate(projectRoot, className, objectDef, includedActions, options, routeDir) {
1035
1217
  const hasGet = includedActions.includes("get");
1036
1218
  const hasPut = includedActions.includes("update");
1037
1219
  const hasDelete = includedActions.includes("delete");
1038
1220
  const simpleClassName = extractSimpleClassName(className);
1221
+ const modelType = resolveObjectTypeReference(
1222
+ projectRoot,
1223
+ className,
1224
+ objectDef,
1225
+ options,
1226
+ routeDir
1227
+ );
1039
1228
  const serializers = resolveStandardRouteSerializers(
1040
1229
  objectDef.decoratorConfig?.api
1041
1230
  );
@@ -1046,13 +1235,14 @@ function generateItemRouteTemplate(_projectRoot, className, objectDef, includedA
1046
1235
  import { error, json } from '@sveltejs/kit';
1047
1236
  ${serializerImports ? `${serializerImports}
1048
1237
  ` : ""}import { getCollection } from '$lib/server/smrt';
1049
- import type { RequestHandler } from './$types';
1238
+ ${modelType.importStatement ? `${modelType.importStatement}
1239
+ ` : ""}import type { RequestHandler } from './$types';
1050
1240
  ${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPut ? generateWritablePolicyHelper(objectDef) : ""}`;
1051
1241
  const getHandler = hasGet ? `
1052
1242
  // Get single ${simpleClassName.toLowerCase()}
1053
1243
  export const GET: RequestHandler = async ({ locals, params }) => {
1054
1244
  ${routeGuardPreamble(objectDef, false)}
1055
- ${generateCollectionLoad(className, { generic: true })}
1245
+ ${generateCollectionLoad(className, { typeName: modelType.typeName })}
1056
1246
  const item = await collection.get(params.id);
1057
1247
  ${generateNotFoundError(className)}
1058
1248
  ${serializers.itemSerializerName ? `
@@ -1066,11 +1256,12 @@ ${serializers.itemSerializerName ? `
1066
1256
  // Update ${simpleClassName.toLowerCase()}
1067
1257
  export const PUT: RequestHandler = async ({ locals, params, request }) => {
1068
1258
  ${routeGuardPreamble(objectDef, true)}
1069
- ${generateCollectionLoad(className, { generic: true })}
1259
+ ${generateCollectionLoad(className, { typeName: modelType.typeName })}
1070
1260
  const item = await collection.get(params.id);
1071
1261
  ${generateNotFoundError(className)}
1072
1262
 
1073
- const data = applyWritablePolicy(await request.json());
1263
+ const body: unknown = await request.json();
1264
+ const data = applyWritablePolicy(body);
1074
1265
  Object.assign(item, data);
1075
1266
  await item.save();
1076
1267
  ${serializers.itemSerializerName ? `
@@ -1084,7 +1275,7 @@ ${serializers.itemSerializerName ? `
1084
1275
  // Delete ${simpleClassName.toLowerCase()}
1085
1276
  export const DELETE: RequestHandler = async ({ locals, params }) => {
1086
1277
  ${routeGuardPreamble(objectDef, true)}
1087
- ${generateCollectionLoad(className, { generic: true })}
1278
+ ${generateCollectionLoad(className, { typeName: modelType.typeName })}
1088
1279
  const item = await collection.get(params.id);
1089
1280
  ${generateNotFoundError(className)}
1090
1281
 
@@ -1094,12 +1285,30 @@ ${generateNotFoundError(className)}
1094
1285
  ` : "";
1095
1286
  return imports + getHandler + putHandler + deleteHandler;
1096
1287
  }
1097
- function generateActionRouteTemplate(_projectRoot, routeSpecs, objectDef, _options) {
1288
+ function generateActionRouteTemplate(projectRoot, routeDir, routeSpecs, objectDef, options) {
1098
1289
  if (routeSpecs.length === 0) {
1099
1290
  throw new Error("Cannot generate a custom action route without handlers");
1100
1291
  }
1101
1292
  const [firstSpec] = routeSpecs;
1102
1293
  const { lookupClassName, routeConfig, hostType } = firstSpec;
1294
+ const lookupModelType = resolveObjectTypeReference(
1295
+ projectRoot,
1296
+ lookupClassName,
1297
+ firstSpec.lookupObjectDef,
1298
+ options,
1299
+ routeDir
1300
+ );
1301
+ const hostModelType = resolveObjectTypeReference(
1302
+ projectRoot,
1303
+ firstSpec.hostClassName,
1304
+ objectDef,
1305
+ options,
1306
+ routeDir
1307
+ );
1308
+ const typeImports = mergeImportStatements([
1309
+ lookupModelType.importStatement,
1310
+ hostModelType.importStatement
1311
+ ]);
1103
1312
  const hasMixedHosts = routeSpecs.some(
1104
1313
  (spec) => spec.hostType !== hostType || spec.lookupClassName !== lookupClassName || spec.routeConfig.scope !== routeConfig.scope
1105
1314
  );
@@ -1108,14 +1317,15 @@ function generateActionRouteTemplate(_projectRoot, routeSpecs, objectDef, _optio
1108
1317
  `Cannot generate mixed custom route handlers for ${lookupClassName}. All handlers sharing a route path must target the same host type and scope.`
1109
1318
  );
1110
1319
  }
1111
- const importBlock = hostType === "collection" ? `import { error, json } from '@sveltejs/kit';
1112
- import { getCollection } from '$lib/server/smrt';
1113
- import type { RequestHandler } from './$types';` : routeConfig.scope === "collection" ? `import { error, json } from '@sveltejs/kit';
1114
- import { ObjectRegistry } from '@happyvertical/smrt-core';
1115
- import type { RequestHandler } from './$types';` : `import { error, json } from '@sveltejs/kit';
1116
- import { getCollection } from '$lib/server/smrt';
1117
- import type { RequestHandler } from './$types';`;
1118
- const tenantScoped = isTenantScoped(objectDef);
1320
+ const importBlock = [
1321
+ "import { error, json } from '@sveltejs/kit';",
1322
+ hostType === "collection" || routeConfig.scope !== "collection" ? "import { getCollection } from '$lib/server/smrt';" : "import { ObjectRegistry } from '@happyvertical/smrt-core';",
1323
+ typeImports,
1324
+ "import type { RequestHandler } from './$types';"
1325
+ ].filter(Boolean).join("\n");
1326
+ const tenantScoped = isTenantScoped(objectDef) || routeSpecs.some(
1327
+ (spec) => !!spec.lookupObjectDef && isTenantScoped(spec.lookupObjectDef)
1328
+ );
1119
1329
  const handlers = routeSpecs.map(
1120
1330
  (spec) => generateActionRouteHandler(
1121
1331
  spec.lookupClassName,
@@ -1123,7 +1333,9 @@ import type { RequestHandler } from './$types';`;
1123
1333
  spec.actionDef,
1124
1334
  spec.routeConfig,
1125
1335
  spec.hostType,
1126
- tenantScoped
1336
+ tenantScoped,
1337
+ lookupModelType.typeName,
1338
+ hostModelType.typeName
1127
1339
  )
1128
1340
  ).join("\n");
1129
1341
  return `${AUTO_GENERATED_ROUTE_HEADER}
@@ -1133,7 +1345,7 @@ ${importBlock}
1133
1345
  ${generateAuthGuardHelper(objectDef)}${tenantScoped ? generateTenantContextHelper() : ""}
1134
1346
  ${handlers}`;
1135
1347
  }
1136
- function generateActionRouteHandler(lookupClassName, actionName, actionDef, routeConfig, hostType, tenantScoped) {
1348
+ function generateActionRouteHandler(lookupClassName, actionName, actionDef, routeConfig, hostType, tenantScoped, lookupTypeName, hostTypeName) {
1137
1349
  const handlerName = routeConfig.method;
1138
1350
  const guardLines = [
1139
1351
  ` requireRouteAuth(locals, ${routeConfig.method !== "GET"});`
@@ -1145,45 +1357,32 @@ function generateActionRouteHandler(lookupClassName, actionName, actionDef, rout
1145
1357
  const hasInput = actionDef.parameters.length > 0;
1146
1358
  const needsRequest = hasInput;
1147
1359
  const invocationArgs = buildActionInvocationArgs(actionDef);
1148
- const hasPathParams = routeConfig.pathParamNames.length > 0;
1149
- const pathParamsObjectLiteral = buildPathParamsObjectLiteral(
1150
- routeConfig.pathParamNames
1151
- );
1152
- const optionsLoad = hasInput && routeConfig.method === "GET" ? hasPathParams ? [
1153
- ` const pathParams = ${pathParamsObjectLiteral};`,
1154
- " const options = {",
1155
- " ...Object.fromEntries(new URL(request.url).searchParams.entries()),",
1156
- " ...pathParams,",
1157
- " };",
1158
- ""
1159
- ].join("\n") : [
1160
- " const options = Object.fromEntries(",
1161
- " new URL(request.url).searchParams.entries(),",
1162
- " );",
1163
- ""
1164
- ].join("\n") : hasInput ? hasPathParams ? [
1165
- ` const pathParams = ${pathParamsObjectLiteral};`,
1166
- " const options = {",
1167
- " ...(await request.json()),",
1168
- " ...pathParams,",
1169
- " };",
1170
- ""
1171
- ].join("\n") : " const options = await request.json();\n" : "";
1172
1360
  const collectionHandlerArgs = buildRouteHandlerArgs(
1173
- hasPathParams,
1361
+ routeConfig.pathParamNames.length > 0,
1174
1362
  needsRequest
1175
1363
  );
1176
1364
  const itemHandlerArgs = buildRouteHandlerArgs(true, needsRequest);
1177
1365
  if (hostType === "collection") {
1366
+ const optionsLoad2 = buildActionOptionsLoad(
1367
+ actionName,
1368
+ actionDef,
1369
+ routeConfig,
1370
+ hostTypeName,
1371
+ actionDef.isStatic
1372
+ );
1373
+ const receiverExpression = actionDef.isStatic ? "CollectionClass" : "typedCollection";
1374
+ const staticTargetLoad = actionDef.isStatic ? ` const CollectionClass = typedCollection.constructor as typeof ${hostTypeName};
1375
+ ` : "";
1178
1376
  return `// Custom collection method: ${actionName}
1179
1377
  export const ${handlerName}: RequestHandler = async (${collectionHandlerArgs}) => {
1180
1378
  ${authGuardLine}
1181
- ${generateCollectionLoad(lookupClassName, { generic: true })}
1182
- const typedCollection = collection as any;
1379
+ ${generateCollectionLoad(lookupClassName, { typeName: lookupTypeName })}
1380
+ const typedCollection = collection as unknown as ${hostTypeName};
1183
1381
  ${generateCollectionNotRegisteredError(lookupClassName)}
1382
+ ${staticTargetLoad}
1184
1383
 
1185
- ${optionsLoad} const result = ${buildActionInvocationExpression(
1186
- actionDef.isStatic ? "(typedCollection.constructor as any)" : "typedCollection",
1384
+ ${optionsLoad2} const result = ${buildActionInvocationExpression(
1385
+ receiverExpression,
1187
1386
  actionName,
1188
1387
  invocationArgs
1189
1388
  )};
@@ -1193,13 +1392,20 @@ ${optionsLoad} const result = ${buildActionInvocationExpression(
1193
1392
  `;
1194
1393
  }
1195
1394
  if (routeConfig.scope === "collection") {
1395
+ const optionsLoad2 = buildActionOptionsLoad(
1396
+ actionName,
1397
+ actionDef,
1398
+ routeConfig,
1399
+ hostTypeName,
1400
+ true
1401
+ );
1196
1402
  return `// Custom collection action: ${actionName}
1197
1403
  export const ${handlerName}: RequestHandler = async (${collectionHandlerArgs}) => {
1198
1404
  ${authGuardLine}
1199
1405
  const registered = ObjectRegistry.getClass('${lookupClassName}');
1200
1406
  ${generateClassNotRegisteredError(lookupClassName)}
1201
1407
 
1202
- ${optionsLoad} const ClassRef = registered.constructor as any;
1408
+ ${optionsLoad2} const ClassRef = registered.constructor as typeof ${hostTypeName};
1203
1409
  const result = ${buildActionInvocationExpression(
1204
1410
  "ClassRef",
1205
1411
  actionName,
@@ -1210,10 +1416,17 @@ ${optionsLoad} const ClassRef = registered.constructor as any;
1210
1416
  };
1211
1417
  `;
1212
1418
  }
1419
+ const optionsLoad = buildActionOptionsLoad(
1420
+ actionName,
1421
+ actionDef,
1422
+ routeConfig,
1423
+ hostTypeName,
1424
+ false
1425
+ );
1213
1426
  return `// Custom action: ${actionName}
1214
1427
  export const ${handlerName}: RequestHandler = async (${itemHandlerArgs}) => {
1215
1428
  ${authGuardLine}
1216
- ${generateCollectionLoad(lookupClassName, { generic: true })}
1429
+ ${generateCollectionLoad(lookupClassName, { typeName: lookupTypeName })}
1217
1430
  const item = await collection.get(params.id);
1218
1431
  ${generateNotFoundError(lookupClassName)}
1219
1432
 
@@ -1243,7 +1456,7 @@ import type { RequestHandler } from './$types';
1243
1456
  const INCLUDE_DOCS_BY_DEFAULT = ${JSON.stringify(includeDocs)};
1244
1457
  const INCLUDE_PROMPTS_BY_DEFAULT = ${JSON.stringify(includePrompts)};
1245
1458
  const REQUIRE_ADMIN = ${JSON.stringify(requireAdmin)};
1246
- const KNOWLEDGE_ARTIFACT: Record<string, any> | null = ${artifactLiteral};
1459
+ const KNOWLEDGE_ARTIFACT: Record<string, unknown> | null = ${artifactLiteral};
1247
1460
 
1248
1461
  if (!dev && !REQUIRE_ADMIN) {
1249
1462
  console.warn('[smrt] PUBLIC knowledge API route enabled; unauthenticated responses are sanitized.');
@@ -1252,7 +1465,7 @@ if (!dev && !REQUIRE_ADMIN) {
1252
1465
  export const GET: RequestHandler = async ({ locals, url, setHeaders }) => {
1253
1466
  setHeaders({ 'cache-control': 'private, no-store' });
1254
1467
 
1255
- if (!dev && REQUIRE_ADMIN && !isKnowledgeAdmin(locals as Record<string, any>)) {
1468
+ if (!dev && REQUIRE_ADMIN && !isKnowledgeAdmin(locals)) {
1256
1469
  throw error(403, 'SMRT knowledge requires dev mode or admin access');
1257
1470
  }
1258
1471
 
@@ -1271,7 +1484,7 @@ export const GET: RequestHandler = async ({ locals, url, setHeaders }) => {
1271
1484
  }));
1272
1485
  };
1273
1486
 
1274
- function readKnowledgeArtifact(): Record<string, any> {
1487
+ function readKnowledgeArtifact(): Record<string, unknown> {
1275
1488
  if (!KNOWLEDGE_ARTIFACT) throw error(404, 'SMRT knowledge artifact not found');
1276
1489
  return KNOWLEDGE_ARTIFACT;
1277
1490
  }
@@ -1283,9 +1496,9 @@ function queryBoolean(url: URL, name: string, defaultValue: boolean): boolean {
1283
1496
  }
1284
1497
 
1285
1498
  function sanitizeKnowledgeArtifact(
1286
- artifact: Record<string, any>,
1499
+ artifact: Record<string, unknown>,
1287
1500
  options: { includeDocs: boolean; includePrompts: boolean; publicAccess: boolean },
1288
- ): Record<string, any> {
1501
+ ): Record<string, unknown> {
1289
1502
  const sanitized = { ...artifact };
1290
1503
 
1291
1504
  if (!options.includeDocs) {
@@ -1306,21 +1519,32 @@ function sanitizeKnowledgeArtifact(
1306
1519
  return sanitized;
1307
1520
  }
1308
1521
 
1309
- function isKnowledgeAdmin(locals: Record<string, any>): boolean {
1310
- if (locals.smrtKnowledgeAdmin === true || locals.smrtAdmin === true) {
1522
+ function recordValue(value: unknown, key: string): unknown {
1523
+ if (!value || typeof value !== 'object') return undefined;
1524
+ return (value as Record<string, unknown>)[key];
1525
+ }
1526
+
1527
+ function isKnowledgeAdmin(locals: unknown): boolean {
1528
+ if (!locals || typeof locals !== 'object') return false;
1529
+ const localRecord = locals as Record<string, unknown>;
1530
+ if (
1531
+ localRecord.smrtKnowledgeAdmin === true ||
1532
+ localRecord.smrtAdmin === true
1533
+ ) {
1311
1534
  return true;
1312
1535
  }
1313
1536
 
1314
- const userRoles = rolesFrom(locals.user);
1315
- const sessionRoles = rolesFrom(locals.session?.user);
1537
+ const userRoles = rolesFrom(localRecord.user);
1538
+ const sessionRoles = rolesFrom(recordValue(localRecord.session, 'user'));
1316
1539
  return [...userRoles, ...sessionRoles].some((role) =>
1317
1540
  ['admin', 'owner', 'superadmin'].includes(role),
1318
1541
  );
1319
1542
  }
1320
1543
 
1321
- function rolesFrom(value: any): string[] {
1544
+ function rolesFrom(value: unknown): string[] {
1322
1545
  if (!value || typeof value !== 'object') return [];
1323
- const roles = Array.isArray(value.roles) ? value.roles : [value.role];
1546
+ const roleList = recordValue(value, 'roles');
1547
+ const roles = Array.isArray(roleList) ? roleList : [recordValue(value, 'role')];
1324
1548
  return roles
1325
1549
  .filter((role): role is string => typeof role === 'string')
1326
1550
  .map((role) => role.toLowerCase());