@warlock.js/core 5.28.0 → 5.29.0

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.
Files changed (79) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/esm/container/development-app-modules.d.mts +31 -0
  3. package/esm/container/development-app-modules.d.mts.map +1 -0
  4. package/esm/container/index.d.mts +2 -0
  5. package/esm/container/index.d.mts.map +1 -1
  6. package/esm/container/index.mjs.map +1 -1
  7. package/esm/dev-server/app-module-registry.mjs +52 -0
  8. package/esm/dev-server/app-module-registry.mjs.map +1 -0
  9. package/esm/dev-server/files-orchestrator.mjs +13 -2
  10. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  11. package/esm/index.d.mts +3 -1
  12. package/esm/index.mjs +2 -1
  13. package/esm/localization/parse-locale-dictionary.d.mts +6 -1
  14. package/esm/localization/parse-locale-dictionary.d.mts.map +1 -1
  15. package/esm/localization/parse-locale-dictionary.mjs +5 -1
  16. package/esm/localization/parse-locale-dictionary.mjs.map +1 -1
  17. package/esm/openapi/response-schemas.mjs +4 -1
  18. package/esm/openapi/response-schemas.mjs.map +1 -1
  19. package/esm/openapi/schema-registry.mjs +6 -0
  20. package/esm/openapi/schema-registry.mjs.map +1 -1
  21. package/esm/resource/index.d.mts +1 -0
  22. package/esm/resource/index.mjs +1 -0
  23. package/esm/resource/nullable-resource.d.mts +44 -0
  24. package/esm/resource/nullable-resource.d.mts.map +1 -0
  25. package/esm/resource/nullable-resource.mjs +38 -0
  26. package/esm/resource/nullable-resource.mjs.map +1 -0
  27. package/esm/resource/resource.d.mts.map +1 -1
  28. package/esm/resource/resource.mjs +22 -3
  29. package/esm/resource/resource.mjs.map +1 -1
  30. package/esm/resource/types.d.mts +10 -5
  31. package/esm/resource/types.d.mts.map +1 -1
  32. package/esm/router/response-schema-types.mjs +2 -0
  33. package/esm/router/response-schema-types.mjs.map +1 -1
  34. package/llms-full.txt +241 -241
  35. package/llms.txt +42 -42
  36. package/package.json +12 -12
  37. package/skills/README.md +39 -39
  38. package/skills/add-connector/SKILL.md +4 -4
  39. package/skills/benchmark-code/SKILL.md +3 -3
  40. package/skills/build-restful/SKILL.md +5 -5
  41. package/skills/build-url/SKILL.md +7 -7
  42. package/skills/configure-app/SKILL.md +7 -7
  43. package/skills/create-controller/SKILL.md +8 -8
  44. package/skills/create-module/SKILL.md +9 -9
  45. package/skills/define-resource/SKILL.md +4 -4
  46. package/skills/encrypt-data/SKILL.md +7 -7
  47. package/skills/generate-openapi/SKILL.md +5 -5
  48. package/skills/hash-password/SKILL.md +8 -8
  49. package/skills/health-checks/SKILL.md +6 -6
  50. package/skills/lower-stage3-decorators/SKILL.md +4 -4
  51. package/skills/process-image/SKILL.md +5 -5
  52. package/skills/register-route/SKILL.md +6 -6
  53. package/skills/request-memo/SKILL.md +4 -4
  54. package/skills/request-tracing/SKILL.md +6 -6
  55. package/skills/resolve-path/SKILL.md +6 -6
  56. package/skills/retry-operation/SKILL.md +4 -4
  57. package/skills/run-app/SKILL.md +11 -11
  58. package/skills/send-mail/SKILL.md +3 -3
  59. package/skills/send-response/SKILL.md +6 -6
  60. package/skills/store-file/SKILL.md +4 -4
  61. package/skills/test-http/SKILL.md +5 -5
  62. package/skills/test-service/SKILL.md +5 -5
  63. package/skills/update-packages/SKILL.md +6 -6
  64. package/skills/upload-file/SKILL.md +7 -7
  65. package/skills/use-app-context/SKILL.md +7 -7
  66. package/skills/use-localization/SKILL.md +6 -6
  67. package/skills/use-middleware/SKILL.md +6 -6
  68. package/skills/use-model-transformers/SKILL.md +6 -6
  69. package/skills/use-repository/SKILL.md +7 -7
  70. package/skills/use-request-locals/SKILL.md +4 -4
  71. package/skills/validate-input/SKILL.md +7 -7
  72. package/skills/warlock-conventions/SKILL.md +6 -6
  73. package/skills/warlock-doctor/SKILL.md +5 -5
  74. package/skills/warlock-routes/SKILL.md +6 -6
  75. package/skills/wire-socket/SKILL.md +4 -4
  76. package/skills/write-cli-command/SKILL.md +6 -6
  77. package/skills/write-middleware/SKILL.md +8 -8
  78. package/skills/write-seeder/SKILL.md +5 -5
  79. package/skills/write-use-case/SKILL.md +3 -3
@@ -1,3 +1,4 @@
1
+ import { isNullableResource } from "../resource/nullable-resource.mjs";
1
2
  import { castToJsonSchema } from "./cast-schema.mjs";
2
3
  import { isLazy } from "@mongez/reinforcements";
3
4
 
@@ -101,7 +102,12 @@ var SchemaRegistry = class {
101
102
  items: context.ownerRef
102
103
  };
103
104
  if (typeof config === "string") return this.cast(config, context.path);
105
+ if (isNullableResource(config)) return { oneOf: [this.fieldSchema(config.resource, context), { type: "null" }] };
104
106
  if (isLazy(config)) return this.lazyFieldSchema(config, context);
107
+ if (Array.isArray(config) && config.length === 1) return {
108
+ type: "array",
109
+ items: this.fieldSchema(config[0], context)
110
+ };
105
111
  if (Array.isArray(config) && config.length === 2 && typeof config[1] === "string") return this.cast(config[1], context.path);
106
112
  if (typeof config === "function") {
107
113
  if (this.isResourceClass(config)) return this.resourceRef(config);
@@ -1 +1 @@
1
- {"version":3,"file":"schema-registry.mjs","names":[],"sources":["../../../../../../../core/src/openapi/schema-registry.ts"],"sourcesContent":["import { isLazy } from \"@mongez/reinforcements\";\nimport { castToJsonSchema } from \"./cast-schema\";\nimport type { OpenApiSchema, WarningSink } from \"./types\";\n\ntype ResourceFieldContext = {\n /** `$ref` schema of the resource whose field is being mapped, the target of `\"self\"`. */\n ownerRef: OpenApiSchema;\n /** Display name used in warnings, for example `UserResource.avatar`. */\n path: string;\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction sanitizeName(name: string): string {\n return name.replace(/[^A-Za-z0-9._-]/g, \"\");\n}\n\n/**\n * Owns `components.schemas`: names resource classes (by identity), builds their schemas once\n * and hands out `$ref`s, so nested, lazy and `\"self\"` references recurse without unrolling.\n */\nexport class SchemaRegistry {\n public readonly schemas: Record<string, OpenApiSchema> = {};\n\n private readonly resourceNames = new Map<unknown, string>();\n private readonly sharedNames = new Map<string, string>();\n private fallbackCounter = 0;\n\n public constructor(\n private readonly resolveName: ((resource: unknown) => string | undefined) | undefined,\n private readonly warn: WarningSink,\n ) {}\n\n /**\n * Register a fixed schema (an error shape shared by many operations) under a stable key.\n * Returns its `$ref`; the component name gets a numeric suffix only if a resource took it.\n */\n public shared(key: string, schema: OpenApiSchema): OpenApiSchema {\n let name = this.sharedNames.get(key);\n\n if (!name) {\n name = this.claim(key);\n this.sharedNames.set(key, name);\n this.schemas[name] = schema;\n }\n\n return { $ref: `#/components/schemas/${name}` };\n }\n\n /**\n * `$ref` to a resource class, building its component on first use.\n */\n public resourceRef(resource: unknown): OpenApiSchema {\n const existing = this.resourceNames.get(resource);\n\n if (existing) {\n return { $ref: `#/components/schemas/${existing}` };\n }\n\n const name = this.claim(this.nameFor(resource));\n const ref = { $ref: `#/components/schemas/${name}` };\n\n // Named before its fields are mapped, so a cycle resolves to this same ref.\n this.resourceNames.set(resource, name);\n this.schemas[name] = {};\n this.schemas[name] = this.buildResourceSchema(resource, name, ref);\n\n return ref;\n }\n\n /**\n * Schema of a cast string, or `{}` and a warning for an unknown cast.\n */\n public cast(cast: string, path: string, warn: WarningSink = this.warn): OpenApiSchema {\n const schema = castToJsonSchema(cast);\n\n if (!schema) {\n warn(`${path}: unknown cast \"${cast}\" is documented as {}.`);\n\n return {};\n }\n\n return schema;\n }\n\n /**\n * True when the function looks like a resource class: it carries a static `schema` object.\n */\n public isResourceClass(value: unknown): boolean {\n return typeof value === \"function\" && isRecord(Reflect.get(value, \"schema\"));\n }\n\n private nameFor(resource: unknown): string {\n const resolved = sanitizeName(this.resolveName?.(resource) ?? \"\");\n\n if (resolved && resolved !== \"default\") {\n return resolved;\n }\n\n const className = typeof resource === \"function\" ? sanitizeName(resource.name) : \"\";\n\n if (className && className !== \"default\") {\n return className;\n }\n\n return `Resource${++this.fallbackCounter}`;\n }\n\n private claim(base: string): string {\n if (!(base in this.schemas)) {\n return base;\n }\n\n let suffix = 2;\n\n while (`${base}${suffix}` in this.schemas) {\n suffix++;\n }\n\n return `${base}${suffix}`;\n }\n\n private buildResourceSchema(resource: unknown, name: string, ref: OpenApiSchema): OpenApiSchema {\n const schema = Reflect.get(resource as object, \"schema\");\n\n if (!isRecord(schema) || Object.keys(schema).length === 0) {\n this.warn(`Resource \"${name}\" declares no static schema and is documented as {}.`);\n\n return {};\n }\n\n const properties: Record<string, OpenApiSchema> = {};\n\n for (const [key, config] of Object.entries(schema)) {\n properties[key] = this.fieldSchema(config, { ownerRef: ref, path: `${name}.${key}` });\n }\n\n return { type: \"object\", properties, required: Object.keys(properties) };\n }\n\n private fieldSchema(config: unknown, context: ResourceFieldContext): OpenApiSchema {\n if (config === \"self\") {\n return context.ownerRef;\n }\n\n if (config === \"self[]\") {\n return { type: \"array\", items: context.ownerRef };\n }\n\n if (typeof config === \"string\") {\n return this.cast(config, context.path);\n }\n\n if (isLazy(config)) {\n return this.lazyFieldSchema(config, context);\n }\n\n if (Array.isArray(config) && config.length === 2 && typeof config[1] === \"string\") {\n return this.cast(config[1], context.path);\n }\n\n if (typeof config === \"function\") {\n if (this.isResourceClass(config)) {\n return this.resourceRef(config);\n }\n\n this.warn(`${context.path}: a resolver function has no static shape and is documented as {}.`);\n\n return {};\n }\n\n if (isRecord(config) && config.__type === \"arrayOf\" && isRecord(config.schema)) {\n const properties: Record<string, OpenApiSchema> = {};\n\n for (const [key, item] of Object.entries(config.schema)) {\n properties[key] = this.fieldSchema(item, { ...context, path: `${context.path}.${key}` });\n }\n\n return {\n type: \"array\",\n items: { type: \"object\", properties, required: Object.keys(properties) },\n };\n }\n\n this.warn(\n `${context.path}: a field builder has no static shape and is documented as {}. Declare it with a cast string to document it.`,\n );\n\n return {};\n }\n\n private lazyFieldSchema(lazy: { resolve(): unknown }, context: ResourceFieldContext): OpenApiSchema {\n let resolved: unknown;\n\n try {\n resolved = lazy.resolve();\n } catch (error) {\n this.warn(\n `${context.path}: a lazy resource reference could not be resolved (${\n error instanceof Error ? error.message : String(error)\n }) and is documented as {}.`,\n );\n\n return {};\n }\n\n return this.fieldSchema(resolved, context);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"schema-registry.mjs","names":[],"sources":["../../../../../../../core/src/openapi/schema-registry.ts"],"sourcesContent":["import { isLazy } from \"@mongez/reinforcements\";\nimport { isNullableResource } from \"../resource/nullable-resource\";\nimport { castToJsonSchema } from \"./cast-schema\";\nimport type { OpenApiSchema, WarningSink } from \"./types\";\n\ntype ResourceFieldContext = {\n /** `$ref` schema of the resource whose field is being mapped, the target of `\"self\"`. */\n ownerRef: OpenApiSchema;\n /** Display name used in warnings, for example `UserResource.avatar`. */\n path: string;\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction sanitizeName(name: string): string {\n return name.replace(/[^A-Za-z0-9._-]/g, \"\");\n}\n\n/**\n * Owns `components.schemas`: names resource classes (by identity), builds their schemas once\n * and hands out `$ref`s, so nested, lazy and `\"self\"` references recurse without unrolling.\n */\nexport class SchemaRegistry {\n public readonly schemas: Record<string, OpenApiSchema> = {};\n\n private readonly resourceNames = new Map<unknown, string>();\n private readonly sharedNames = new Map<string, string>();\n private fallbackCounter = 0;\n\n public constructor(\n private readonly resolveName: ((resource: unknown) => string | undefined) | undefined,\n private readonly warn: WarningSink,\n ) {}\n\n /**\n * Register a fixed schema (an error shape shared by many operations) under a stable key.\n * Returns its `$ref`; the component name gets a numeric suffix only if a resource took it.\n */\n public shared(key: string, schema: OpenApiSchema): OpenApiSchema {\n let name = this.sharedNames.get(key);\n\n if (!name) {\n name = this.claim(key);\n this.sharedNames.set(key, name);\n this.schemas[name] = schema;\n }\n\n return { $ref: `#/components/schemas/${name}` };\n }\n\n /**\n * `$ref` to a resource class, building its component on first use.\n */\n public resourceRef(resource: unknown): OpenApiSchema {\n const existing = this.resourceNames.get(resource);\n\n if (existing) {\n return { $ref: `#/components/schemas/${existing}` };\n }\n\n const name = this.claim(this.nameFor(resource));\n const ref = { $ref: `#/components/schemas/${name}` };\n\n // Named before its fields are mapped, so a cycle resolves to this same ref.\n this.resourceNames.set(resource, name);\n this.schemas[name] = {};\n this.schemas[name] = this.buildResourceSchema(resource, name, ref);\n\n return ref;\n }\n\n /**\n * Schema of a cast string, or `{}` and a warning for an unknown cast.\n */\n public cast(cast: string, path: string, warn: WarningSink = this.warn): OpenApiSchema {\n const schema = castToJsonSchema(cast);\n\n if (!schema) {\n warn(`${path}: unknown cast \"${cast}\" is documented as {}.`);\n\n return {};\n }\n\n return schema;\n }\n\n /**\n * True when the function looks like a resource class: it carries a static `schema` object.\n */\n public isResourceClass(value: unknown): boolean {\n return typeof value === \"function\" && isRecord(Reflect.get(value, \"schema\"));\n }\n\n private nameFor(resource: unknown): string {\n const resolved = sanitizeName(this.resolveName?.(resource) ?? \"\");\n\n if (resolved && resolved !== \"default\") {\n return resolved;\n }\n\n const className = typeof resource === \"function\" ? sanitizeName(resource.name) : \"\";\n\n if (className && className !== \"default\") {\n return className;\n }\n\n return `Resource${++this.fallbackCounter}`;\n }\n\n private claim(base: string): string {\n if (!(base in this.schemas)) {\n return base;\n }\n\n let suffix = 2;\n\n while (`${base}${suffix}` in this.schemas) {\n suffix++;\n }\n\n return `${base}${suffix}`;\n }\n\n private buildResourceSchema(resource: unknown, name: string, ref: OpenApiSchema): OpenApiSchema {\n const schema = Reflect.get(resource as object, \"schema\");\n\n if (!isRecord(schema) || Object.keys(schema).length === 0) {\n this.warn(`Resource \"${name}\" declares no static schema and is documented as {}.`);\n\n return {};\n }\n\n const properties: Record<string, OpenApiSchema> = {};\n\n for (const [key, config] of Object.entries(schema)) {\n properties[key] = this.fieldSchema(config, { ownerRef: ref, path: `${name}.${key}` });\n }\n\n return { type: \"object\", properties, required: Object.keys(properties) };\n }\n\n private fieldSchema(config: unknown, context: ResourceFieldContext): OpenApiSchema {\n if (config === \"self\") {\n return context.ownerRef;\n }\n\n if (config === \"self[]\") {\n return { type: \"array\", items: context.ownerRef };\n }\n\n if (typeof config === \"string\") {\n return this.cast(config, context.path);\n }\n\n if (isNullableResource(config)) {\n return { oneOf: [this.fieldSchema(config.resource, context), { type: \"null\" }] };\n }\n\n if (isLazy(config)) {\n return this.lazyFieldSchema(config, context);\n }\n\n if (Array.isArray(config) && config.length === 1) {\n return { type: \"array\", items: this.fieldSchema(config[0], context) };\n }\n\n if (Array.isArray(config) && config.length === 2 && typeof config[1] === \"string\") {\n return this.cast(config[1], context.path);\n }\n\n if (typeof config === \"function\") {\n if (this.isResourceClass(config)) {\n return this.resourceRef(config);\n }\n\n this.warn(`${context.path}: a resolver function has no static shape and is documented as {}.`);\n\n return {};\n }\n\n if (isRecord(config) && config.__type === \"arrayOf\" && isRecord(config.schema)) {\n const properties: Record<string, OpenApiSchema> = {};\n\n for (const [key, item] of Object.entries(config.schema)) {\n properties[key] = this.fieldSchema(item, { ...context, path: `${context.path}.${key}` });\n }\n\n return {\n type: \"array\",\n items: { type: \"object\", properties, required: Object.keys(properties) },\n };\n }\n\n this.warn(\n `${context.path}: a field builder has no static shape and is documented as {}. Declare it with a cast string to document it.`,\n );\n\n return {};\n }\n\n private lazyFieldSchema(lazy: { resolve(): unknown }, context: ResourceFieldContext): OpenApiSchema {\n let resolved: unknown;\n\n try {\n resolved = lazy.resolve();\n } catch (error) {\n this.warn(\n `${context.path}: a lazy resource reference could not be resolved (${\n error instanceof Error ? error.message : String(error)\n }) and is documented as {}.`,\n );\n\n return {};\n }\n\n return this.fieldSchema(resolved, context);\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { ResourceFieldBuilder } from "./resource-field-builder.mjs";
2
2
  import { Resource, ResourceConstructor, ResourceContract } from "./resource.mjs";
3
+ import { NULLABLE_RESOURCE, NullableResource, NullableResourceTarget, isNullableResource, nullable } from "./nullable-resource.mjs";
3
4
  import { BaseCastOutput, CastOutput, FieldOutput, ResourceArraySchema, ResourceCastType, ResourceFieldBuilderDateOutputOptions, ResourceFieldConfig, ResourceOutput, ResourceOutputOf, ResourceOutputValueCastType, ResourceSchema, ResourceSelfReference, ResponseBodyOutput, ResponseBodyValue, ResponseSchema, ResponseSchemaOutput } from "./types.mjs";
4
5
  import { DefineResourceOptions, defineResource } from "./define-resource.mjs";
5
6
  import { RegisterResource } from "./register-resource.mjs";
@@ -1,3 +1,4 @@
1
+ import { NULLABLE_RESOURCE, isNullableResource, nullable } from "./nullable-resource.mjs";
1
2
  import { ResourceFieldBuilder } from "./resource-field-builder.mjs";
2
3
  import { Resource } from "./resource.mjs";
3
4
  import { defineResource } from "./define-resource.mjs";
@@ -0,0 +1,44 @@
1
+ import { ResourceConstructor } from "./resource.mjs";
2
+ import { Lazy } from "@mongez/reinforcements";
3
+
4
+ //#region ../core/src/resource/nullable-resource.d.ts
5
+ /**
6
+ * Brand of a nullable resource marker. `Symbol.for` keeps it identical across duplicated
7
+ * copies of this module (the dev server, the typings child process and the app all load one).
8
+ */
9
+ declare const NULLABLE_RESOURCE: unique symbol;
10
+ /**
11
+ * What `nullable()` accepts: a resource class, or a lazy reference to one (for cyclic resources).
12
+ */
13
+ type NullableResourceTarget = ResourceConstructor<any> | Lazy<ResourceConstructor<any>>;
14
+ /**
15
+ * Marker returned by `nullable(Resource)`.
16
+ */
17
+ interface NullableResource<R extends NullableResourceTarget = NullableResourceTarget> {
18
+ readonly [NULLABLE_RESOURCE]: true;
19
+ readonly resource: R;
20
+ }
21
+ /**
22
+ * Declare a nested resource that is always present in the output: the resource's output when
23
+ * there is a value, `null` when the input is `null` or `undefined` (the field is not omitted).
24
+ *
25
+ * Accepted wherever a resource is accepted: as a `defineResource` schema field and as a value in
26
+ * a `responseSchema` body (including nested objects).
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * const PostResource = defineResource({
31
+ * schema: { id: "number", author: nullable(UserResource) },
32
+ * });
33
+ *
34
+ * // ResourceOutput<typeof PostResource> -> { id: number; author: UserJson | null }
35
+ * ```
36
+ */
37
+ declare function nullable<R extends NullableResourceTarget>(resource: R): NullableResource<R>;
38
+ /**
39
+ * True when the value is a marker created by `nullable()`.
40
+ */
41
+ declare function isNullableResource(value: unknown): value is NullableResource;
42
+ //#endregion
43
+ export { NULLABLE_RESOURCE, NullableResource, NullableResourceTarget, isNullableResource, nullable };
44
+ //# sourceMappingURL=nullable-resource.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nullable-resource.d.mts","names":[],"sources":["../../../../../../../core/src/resource/nullable-resource.ts"],"mappings":";;;;;;AAOA;;cAAa,iBAAA;;AAA2D;AAKxE;KAAY,sBAAA,GAAyB,mBAAA,QAA2B,IAAA,CAAK,mBAAA;;;;UAKpD,gBAAA,WAA2B,sBAAA,GAAyB,sBAAA;EAAA,UACzD,iBAAA;EAAA,SACD,QAAA,EAAU,CAAA;AAAA;;;;AAPmE;AAKxF;;;;;;;;;;;;iBAqBgB,QAAA,WAAmB,sBAAA,EAAwB,QAAA,EAAU,CAAA,GAAI,gBAAA,CAAiB,CAAA;;;;iBAO1E,kBAAA,CAAmB,KAAA,YAAiB,KAAA,IAAS,gBAAgB"}
@@ -0,0 +1,38 @@
1
+ //#region ../core/src/resource/nullable-resource.ts
2
+ /**
3
+ * Brand of a nullable resource marker. `Symbol.for` keeps it identical across duplicated
4
+ * copies of this module (the dev server, the typings child process and the app all load one).
5
+ */
6
+ const NULLABLE_RESOURCE = Symbol.for("warlock.resource.nullable");
7
+ /**
8
+ * Declare a nested resource that is always present in the output: the resource's output when
9
+ * there is a value, `null` when the input is `null` or `undefined` (the field is not omitted).
10
+ *
11
+ * Accepted wherever a resource is accepted: as a `defineResource` schema field and as a value in
12
+ * a `responseSchema` body (including nested objects).
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const PostResource = defineResource({
17
+ * schema: { id: "number", author: nullable(UserResource) },
18
+ * });
19
+ *
20
+ * // ResourceOutput<typeof PostResource> -> { id: number; author: UserJson | null }
21
+ * ```
22
+ */
23
+ function nullable(resource) {
24
+ return {
25
+ [NULLABLE_RESOURCE]: true,
26
+ resource
27
+ };
28
+ }
29
+ /**
30
+ * True when the value is a marker created by `nullable()`.
31
+ */
32
+ function isNullableResource(value) {
33
+ return typeof value === "object" && value !== null && value[NULLABLE_RESOURCE] === true;
34
+ }
35
+
36
+ //#endregion
37
+ export { NULLABLE_RESOURCE, isNullableResource, nullable };
38
+ //# sourceMappingURL=nullable-resource.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nullable-resource.mjs","names":[],"sources":["../../../../../../../core/src/resource/nullable-resource.ts"],"sourcesContent":["import { type Lazy } from \"@mongez/reinforcements\";\nimport { type ResourceConstructor } from \"./resource\";\n\n/**\n * Brand of a nullable resource marker. `Symbol.for` keeps it identical across duplicated\n * copies of this module (the dev server, the typings child process and the app all load one).\n */\nexport const NULLABLE_RESOURCE = Symbol.for(\"warlock.resource.nullable\");\n\n/**\n * What `nullable()` accepts: a resource class, or a lazy reference to one (for cyclic resources).\n */\nexport type NullableResourceTarget = ResourceConstructor<any> | Lazy<ResourceConstructor<any>>;\n\n/**\n * Marker returned by `nullable(Resource)`.\n */\nexport interface NullableResource<R extends NullableResourceTarget = NullableResourceTarget> {\n readonly [NULLABLE_RESOURCE]: true;\n readonly resource: R;\n}\n\n/**\n * Declare a nested resource that is always present in the output: the resource's output when\n * there is a value, `null` when the input is `null` or `undefined` (the field is not omitted).\n *\n * Accepted wherever a resource is accepted: as a `defineResource` schema field and as a value in\n * a `responseSchema` body (including nested objects).\n *\n * @example\n * ```typescript\n * const PostResource = defineResource({\n * schema: { id: \"number\", author: nullable(UserResource) },\n * });\n *\n * // ResourceOutput<typeof PostResource> -> { id: number; author: UserJson | null }\n * ```\n */\nexport function nullable<R extends NullableResourceTarget>(resource: R): NullableResource<R> {\n return { [NULLABLE_RESOURCE]: true, resource };\n}\n\n/**\n * True when the value is a marker created by `nullable()`.\n */\nexport function isNullableResource(value: unknown): value is NullableResource {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as Record<symbol, unknown>)[NULLABLE_RESOURCE] === true\n );\n}\n"],"mappings":";;;;;AAOA,MAAa,oBAAoB,OAAO,IAAI,2BAA2B;;;;;;;;;;;;;;;;;AA+BvE,SAAgB,SAA2C,UAAkC;CAC3F,OAAO;GAAG,oBAAoB;EAAM;CAAS;AAC/C;;;;AAKA,SAAgB,mBAAmB,OAA2C;CAC5E,OACE,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,uBAAuB;AAE9D"}
@@ -1 +1 @@
1
- {"version":3,"file":"resource.d.mts","names":[],"sources":["../../../../../../../core/src/resource/resource.ts"],"mappings":"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"}
1
+ {"version":3,"file":"resource.d.mts","names":[],"sources":["../../../../../../../core/src/resource/resource.ts"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { useRequestStore } from "../http/context/request-context.mjs";
2
+ import { isNullableResource } from "./nullable-resource.mjs";
2
3
  import { ResourceFieldBuilder } from "./resource-field-builder.mjs";
3
4
  import { Model } from "@warlock.js/cascade";
4
5
  import { get, isLazy, set } from "@mongez/reinforcements";
@@ -9,6 +10,12 @@ import { get, isLazy, set } from "@mongez/reinforcements";
9
10
  * Prevents runaway serialization on deep trees or circular data.
10
11
  */
11
12
  const MAX_SELF_DEPTH = 10;
13
+ /**
14
+ * Whether the given schema entry is an `arrayOf` item schema
15
+ */
16
+ function isArrayOfSchema(value) {
17
+ return typeof value === "object" && value !== null && !Array.isArray(value) && value.__type === "arrayOf" && typeof value.schema === "object" && value.schema !== null;
18
+ }
12
19
  var Resource = class Resource {
13
20
  static {
14
21
  this.schema = {};
@@ -26,11 +33,15 @@ var Resource = class Resource {
26
33
  for (const [key, value] of Object.entries(schema)) if (value === "self" || value === "self[]") parsed[key] = value;
27
34
  else if (isLazy(value)) parsed[key] = value;
28
35
  else if (typeof value === "string") parsed[key] = ResourceFieldBuilder.fromCastType(value);
29
- else if (Array.isArray(value) && value.length === 2 && typeof value[0] === "string") {
36
+ else if (Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "string") {
30
37
  const builder = ResourceFieldBuilder.fromCastType(value[1]);
31
38
  builder.setInputKey(value[0]);
32
39
  parsed[key] = builder;
33
- } else parsed[key] = value;
40
+ } else if (isArrayOfSchema(value)) parsed[key] = {
41
+ ...value,
42
+ schema: Resource.normalizeSchema(value.schema)
43
+ };
44
+ else parsed[key] = value;
34
45
  return parsed;
35
46
  }
36
47
  /**
@@ -102,6 +113,14 @@ var Resource = class Resource {
102
113
  transformValue(value, outputSettings, locale) {
103
114
  let outputValue;
104
115
  if (isLazy(outputSettings)) return this.transformValue(value, outputSettings.resolve(), locale);
116
+ if (isNullableResource(outputSettings)) {
117
+ if (value === null || value === void 0) return null;
118
+ return this.transformValue(value, outputSettings.resource, locale) ?? null;
119
+ }
120
+ if (Array.isArray(outputSettings) && outputSettings.length === 1) {
121
+ if (!Array.isArray(value)) return;
122
+ return value.map((item) => this.transformValue(item, outputSettings[0], locale)).filter((v) => v !== void 0);
123
+ }
105
124
  if (outputSettings === "self" || outputSettings === "self[]") outputValue = this.transformSelfReference(value, outputSettings === "self[]");
106
125
  else if (typeof outputSettings === "function" && outputSettings.prototype instanceof Resource) {
107
126
  if (!value) return;
@@ -150,7 +169,7 @@ var Resource = class Resource {
150
169
  for (const [outputKey, outputSettings] of Object.entries(schema)) {
151
170
  let fieldKey = outputKey;
152
171
  let valueTransformType = outputSettings;
153
- if (Array.isArray(outputSettings)) {
172
+ if (Array.isArray(outputSettings) && outputSettings.length === 2) {
154
173
  fieldKey = outputSettings[0];
155
174
  valueTransformType = outputSettings[1];
156
175
  }
@@ -1 +1 @@
1
- {"version":3,"file":"resource.mjs","names":[],"sources":["../../../../../../../core/src/resource/resource.ts"],"sourcesContent":["import { get, isLazy, set, type GenericObject } from \"@mongez/reinforcements\";\nimport { Model } from \"@warlock.js/cascade\";\nimport { useRequestStore } from \"../http/context/request-context\";\nimport { ResourceFieldBuilder } from \"./resource-field-builder\";\nimport {\n type ResourceArraySchema,\n type ResourceFieldConfig,\n type ResourceOutputValueCastType,\n type ResourceSchema,\n} from \"./types\";\n\n/**\n * Maximum recursion depth for self-referencing fields.\n * Prevents runaway serialization on deep trees or circular data.\n */\nconst MAX_SELF_DEPTH = 10;\n\n/**\n * Resource contract\n */\nexport interface ResourceContract<Out = GenericObject> {\n /**\n * Resource data\n */\n resource: GenericObject;\n\n /**\n * Resource final output\n */\n data: GenericObject;\n\n /**\n * Original data\n */\n originalData: GenericObject;\n\n /**\n * Convert resource to JSON\n */\n toJSON(): Out;\n\n /**\n * Transform the given value with given type\n */\n transform(value: any, type: ResourceOutputValueCastType, locale?: string): any;\n\n /**\n * Get a input value for the given key\n */\n get(key: string, defaultValue?: any): any;\n\n /**\n * Set the given value for the given field\n */\n set(key: string, value: any): ResourceContract;\n\n /**\n * Create an array schema for transforming array items\n */\n arrayOf(schema: Record<string, ResourceFieldConfig>): ResourceArraySchema;\n\n /**\n * Get a string field builder\n */\n string(inputKey?: string): ResourceFieldBuilder;\n\n /**\n * Get a date field builder\n */\n date(inputKey?: string): ResourceFieldBuilder;\n\n /**\n * Get a localized field builder\n */\n localized(inputKey?: string): ResourceFieldBuilder;\n\n /**\n * Get a url field builder\n */\n url(inputKey?: string): ResourceFieldBuilder;\n\n /**\n * Get a uploadsUrl field builder\n */\n uploadsUrl(inputKey?: string): ResourceFieldBuilder;\n\n /**\n * Get a number field builder\n */\n number(inputKey?: string): ResourceFieldBuilder;\n\n /**\n * Get a boolean field builder\n */\n boolean(inputKey?: string): ResourceFieldBuilder;\n\n /**\n * Get a float field builder\n */\n float(inputKey?: string): ResourceFieldBuilder;\n\n /**\n * Get a int field builder\n */\n int(inputKey?: string): ResourceFieldBuilder;\n}\n\n/**\n * Resource constructor\n */\nexport interface ResourceConstructor<Out = GenericObject> {\n new (originalData: GenericObject | Resource | Model): ResourceContract<Out>;\n}\n\nexport class Resource implements ResourceContract {\n /**\n * Resource data\n */\n public resource: GenericObject = {};\n\n /**\n * Resource final output\n */\n public data: GenericObject = {};\n\n /**\n * Tracks visited object identities during self-reference recursion.\n * Prevents infinite loops on circular data (e.g. A.parent → B, B.parent → A).\n */\n protected _selfSeen?: Set<unknown>;\n\n /**\n * Raw resource schema — field declarations as written by the developer.\n * Used by doc generators for introspection.\n */\n public static schema: Record<string, ResourceFieldConfig | string> = {};\n\n /**\n * Normalized schema — all string cast types and tuples converted to\n * ResourceFieldBuilder instances at definition time. Used by transformOutput at runtime.\n */\n public static parsedSchema: Record<string, ResourceFieldConfig> = {};\n\n /**\n * Normalize a raw schema into parsedSchema.\n * Converts string cast types (including suffixes) and tuples into pre-built builders.\n * Other entry types (ResourceConstructor, resolver functions, ResourceArraySchema) are kept as-is.\n */\n public static normalizeSchema(schema: Record<string, ResourceFieldConfig | string>): Record<string, ResourceFieldConfig> {\n const parsed: Record<string, ResourceFieldConfig> = {};\n\n for (const [key, value] of Object.entries(schema)) {\n if (value === \"self\" || value === \"self[]\") {\n // Keep self-references as-is — resolved lazily in transformValue\n parsed[key] = value;\n } else if (isLazy<ResourceConstructor>(value)) {\n // Keep Lazy<ResourceConstructor> as-is — resolved at transform\n // time so cyclic resource refs (A embeds B embeds A) compose\n // without import-graph cycles.\n parsed[key] = value;\n } else if (typeof value === \"string\") {\n parsed[key] = ResourceFieldBuilder.fromCastType(value);\n } else if (Array.isArray(value) && value.length === 2 && typeof value[0] === \"string\") {\n const builder = ResourceFieldBuilder.fromCastType(value[1] as string);\n builder.setInputKey(value[0]);\n parsed[key] = builder;\n } else {\n parsed[key] = value;\n }\n }\n\n return parsed;\n }\n\n /**\n * Constructor\n */\n public constructor(public originalData: GenericObject | Resource | Model) {\n if (this.originalData instanceof Model) {\n this.resource = this.originalData.data;\n } else if (this.originalData instanceof Resource) {\n this.resource = this.originalData.data;\n } else {\n this.resource = this.originalData;\n }\n }\n\n /**\n * Convert resource to JSON\n */\n public toJSON() {\n this.boot();\n this.transformOutput();\n this.extend();\n\n return this.data;\n }\n\n /**\n * Boot method\n * Called before transforming the resource\n */\n protected boot() {\n //\n }\n\n /**\n * Transform resource to output using the pre-normalized parsedSchema.\n * Builders handle their own array/nullable logic internally.\n * ResourceConstructor and ResourceArraySchema handle arrays in transformValue.\n */\n protected transformOutput() {\n const localeCode = useRequestStore()?.request?.locale;\n const parsedSchema = this.resolveParsedSchema();\n // A model is read through its accessors; anything else through the\n // constructor's normalized input (a wrapped resource's serialized data).\n const source = this.originalData instanceof Model ? this.originalData : this.resource;\n\n for (const [outputKey, outputSettings] of Object.entries(parsedSchema)) {\n const inputKey = outputSettings instanceof ResourceFieldBuilder\n ? outputSettings.getInputKey()\n : undefined;\n const inputValue = this.resolveSourceValue(source, outputKey, inputKey);\n const outputValue = this.transformValue(inputValue, outputSettings, localeCode);\n\n if (outputValue !== undefined) {\n this.set(outputKey, outputValue);\n }\n }\n }\n\n /**\n * Resolve the normalized schema of THIS class.\n * `@RegisterResource()` is optional: when the class has no own `parsedSchema`\n * (a static field is inherited from the base as `{}`), normalize its own\n * static `schema` (or an instance `schema` field) once and cache it on the class.\n */\n protected resolveParsedSchema(): Record<string, ResourceFieldConfig> {\n const ctor = this.constructor as typeof Resource;\n\n if (Object.hasOwn(ctor, \"parsedSchema\")) {\n return ctor.parsedSchema;\n }\n\n const instanceSchema = (this as unknown as { schema?: Record<string, ResourceFieldConfig | string> }).schema;\n const schema = instanceSchema ?? ctor.schema;\n\n if (!schema || Object.keys(schema).length === 0) {\n throw new Error(\n `Resource \"${ctor.name}\" has no schema. Declare \\`static schema = { ... }\\` on the class.`,\n );\n }\n\n const parsed = Resource.normalizeSchema(schema);\n\n // Instance schemas are per-class by convention, so caching on the class is safe\n ctor.parsedSchema = parsed;\n\n return parsed;\n }\n\n /**\n * Transform the given value with given type\n */\n public transform(value: any, type: ResourceOutputValueCastType, locale?: string) {\n return new ResourceFieldBuilder(type).transform(value, locale);\n }\n\n /**\n * Transform the given value for the given output setting.\n * After normalization, string cast types no longer reach here — they are pre-converted to builders.\n */\n protected transformValue(value: any, outputSettings: ResourceFieldConfig, locale?: string): any {\n let outputValue: any;\n\n if (isLazy<ResourceConstructor>(outputSettings)) {\n // Resolve the lazy ref now (cyclic resource imports are fully loaded\n // by serialization time) and recurse — the resolved constructor is\n // handled by the ResourceConstructor branch below.\n return this.transformValue(value, outputSettings.resolve(), locale);\n }\n\n if (outputSettings === \"self\" || outputSettings === \"self[]\") {\n // Self-reference — resolve using the same resource class with cycle detection\n outputValue = this.transformSelfReference(value, outputSettings === \"self[]\");\n } else if (\n typeof outputSettings === \"function\" &&\n outputSettings.prototype instanceof Resource\n ) {\n if (!value) return;\n // Nested resource — handle both single and array values\n if (Array.isArray(value)) {\n // An empty list stays an empty list ([] is the honest output); only a\n // missing/null input omits the field.\n outputValue = value\n .map((item) => new (outputSettings as typeof Resource)(item).toJSON())\n .filter((v) => v !== undefined);\n } else {\n outputValue = new (outputSettings as typeof Resource)(value).toJSON();\n }\n } else if (typeof outputSettings === \"function\") {\n // Resolver function - bind to Resource instance for access to this.get(), etc.\n outputValue = (outputSettings as Function).call(this, value, this);\n } else if (outputSettings instanceof ResourceFieldBuilder) {\n // Builder — handles array mapping and nullable internally via isArrayField\n outputValue = outputSettings.transform(value, locale);\n } else if (\n typeof outputSettings === \"object\" &&\n outputSettings !== null &&\n \"__type\" in outputSettings &&\n outputSettings.__type === \"arrayOf\"\n ) {\n // ResourceArraySchema — structured array items with their own sub-schema\n if (Array.isArray(value)) {\n outputValue = value\n .map((item) => this.transformArrayItem(item, outputSettings.schema, locale))\n .filter((v) => v !== undefined);\n } else {\n outputValue = this.transformArrayItem(value, outputSettings.schema, locale);\n }\n }\n\n return outputValue;\n }\n\n /**\n * Extend the resource output\n */\n protected extend() {\n //\n }\n\n /**\n * Transform a self-referencing field value.\n *\n * @example\n * // Single: parent: \"self\"\n * // Array: children: \"self[]\"\n */\n protected transformSelfReference(value: any, isArray: boolean): any {\n if (isArray) {\n return Array.isArray(value)\n ? value.map((item) => this.resolveSelf(item)).filter((v) => v !== undefined)\n : undefined;\n }\n\n return this.resolveSelf(value);\n }\n\n /**\n * Resolve a single self-reference value.\n * Uses identity-based cycle detection (id/_id) and a max depth guard.\n */\n protected resolveSelf(value: any): any {\n if (!value) return undefined;\n\n const identity = value.id ?? value._id ?? value;\n // Ancestors of the current path only: siblings never see each other,\n // so the set size is the depth.\n const ancestors = this._selfSeen ?? new Set();\n\n // Circular reference or depth limit reached — stop recursion\n if (ancestors.has(identity) || ancestors.size >= MAX_SELF_DEPTH) {\n return undefined;\n }\n\n const SelfConstructor = this.constructor as typeof Resource;\n const child = new SelfConstructor(value);\n child._selfSeen = new Set(ancestors).add(identity);\n\n return child.toJSON();\n }\n\n /**\n * Transform a single array item according to the given schema\n */\n protected transformArrayItem(\n item: any,\n schema: Record<string, ResourceFieldConfig>,\n locale?: string,\n ) {\n const transformedItem: GenericObject = {};\n\n for (const [outputKey, outputSettings] of Object.entries(schema)) {\n let fieldKey = outputKey;\n let valueTransformType = outputSettings as ResourceFieldConfig;\n\n if (Array.isArray(outputSettings)) {\n fieldKey = outputSettings[0];\n valueTransformType = outputSettings[1];\n }\n\n const inputKey = valueTransformType instanceof ResourceFieldBuilder\n ? valueTransformType.getInputKey()\n : undefined;\n const inputValue = this.resolveSourceValue(item, outputKey, inputKey ?? fieldKey);\n const outputValue = this.transformValue(inputValue, valueTransformType, locale);\n\n if (outputValue !== undefined) {\n set(transformedItem, outputKey, outputValue);\n }\n }\n\n return transformedItem;\n }\n\n /**\n * Resolve one schema field from a model or plain object. Mongo records use `_id`\n * internally, while resources expose `id` by convention.\n */\n protected resolveSourceValue(source: GenericObject | Resource | Model, outputKey: string, inputKey?: string) {\n const key = inputKey ?? outputKey;\n const read = (field: string) => source instanceof Model ? source.get(field) : get(source, field);\n const value = read(key);\n\n return outputKey === \"id\" && key === \"id\" && value === undefined ? read(\"_id\") : value;\n }\n\n /**\n * Get a input value for the given key\n */\n public get(key: string, defaultValue?: any) {\n return get(this.resource, key, defaultValue);\n }\n\n /**\n * Set the given value for the given field\n */\n public set(key: string, value: any): ResourceContract {\n set(this.data, key, value);\n\n return this;\n }\n\n /**\n * Create an array schema for transforming array items\n */\n public arrayOf(schema: Record<string, ResourceFieldConfig>): ResourceArraySchema {\n return {\n __type: \"arrayOf\",\n schema,\n };\n }\n\n /**\n * Get a string field builder\n */\n public string(inputKey?: string) {\n return this.fieldBuilder(\"string\", inputKey);\n }\n\n /**\n * Get a date field builder\n */\n public date(inputKey?: string) {\n return this.fieldBuilder(\"date\", inputKey);\n }\n\n /**\n * Get a localized field builder\n */\n public localized(inputKey?: string) {\n return this.fieldBuilder(\"localized\", inputKey);\n }\n\n /**\n * Get a url field builder\n */\n public url(inputKey?: string) {\n return this.fieldBuilder(\"url\", inputKey);\n }\n\n /**\n * Get a uploadsUrl field builder\n */\n public uploadsUrl(inputKey?: string) {\n return this.fieldBuilder(\"uploadsUrl\", inputKey);\n }\n\n /**\n * Get a number field builder\n */\n public number(inputKey?: string) {\n return this.fieldBuilder(\"number\", inputKey);\n }\n\n /**\n * Get a boolean field builder\n */\n public boolean(inputKey?: string) {\n return this.fieldBuilder(\"boolean\", inputKey);\n }\n\n /**\n * Get a float field builder\n */\n public float(inputKey?: string) {\n return this.fieldBuilder(\"float\", inputKey);\n }\n\n /**\n * Get a int field builder\n */\n public int(inputKey?: string) {\n return this.fieldBuilder(\"int\", inputKey);\n }\n\n /**\n * New field builder\n */\n protected fieldBuilder(type: ResourceOutputValueCastType, inputKey?: string) {\n const builder = new ResourceFieldBuilder(type);\n\n if (inputKey) {\n builder.setInputKey(inputKey);\n }\n\n return builder;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"resource.mjs","names":[],"sources":["../../../../../../../core/src/resource/resource.ts"],"sourcesContent":["import { get, isLazy, set, type GenericObject } from \"@mongez/reinforcements\";\nimport { Model } from \"@warlock.js/cascade\";\nimport { useRequestStore } from \"../http/context/request-context\";\nimport { isNullableResource } from \"./nullable-resource\";\nimport { ResourceFieldBuilder } from \"./resource-field-builder\";\nimport {\n type ResourceArraySchema,\n type ResourceFieldConfig,\n type ResourceOutputValueCastType,\n type ResourceSchema,\n} from \"./types\";\n\n/**\n * Maximum recursion depth for self-referencing fields.\n * Prevents runaway serialization on deep trees or circular data.\n */\nconst MAX_SELF_DEPTH = 10;\n\n/**\n * Whether the given schema entry is an `arrayOf` item schema\n */\nfunction isArrayOfSchema(value: unknown): value is ResourceArraySchema {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n (value as { __type?: unknown }).__type === \"arrayOf\" &&\n typeof (value as { schema?: unknown }).schema === \"object\" &&\n (value as { schema?: unknown }).schema !== null\n );\n}\n\n/**\n * Resource contract\n */\nexport interface ResourceContract<Out = GenericObject> {\n /**\n * Resource data\n */\n resource: GenericObject;\n\n /**\n * Resource final output\n */\n data: GenericObject;\n\n /**\n * Original data\n */\n originalData: GenericObject;\n\n /**\n * Convert resource to JSON\n */\n toJSON(): Out;\n\n /**\n * Transform the given value with given type\n */\n transform(value: any, type: ResourceOutputValueCastType, locale?: string): any;\n\n /**\n * Get a input value for the given key\n */\n get(key: string, defaultValue?: any): any;\n\n /**\n * Set the given value for the given field\n */\n set(key: string, value: any): ResourceContract;\n\n /**\n * Create an array schema for transforming array items\n */\n arrayOf(schema: Record<string, ResourceFieldConfig>): ResourceArraySchema;\n\n /**\n * Get a string field builder\n */\n string(inputKey?: string): ResourceFieldBuilder;\n\n /**\n * Get a date field builder\n */\n date(inputKey?: string): ResourceFieldBuilder;\n\n /**\n * Get a localized field builder\n */\n localized(inputKey?: string): ResourceFieldBuilder;\n\n /**\n * Get a url field builder\n */\n url(inputKey?: string): ResourceFieldBuilder;\n\n /**\n * Get a uploadsUrl field builder\n */\n uploadsUrl(inputKey?: string): ResourceFieldBuilder;\n\n /**\n * Get a number field builder\n */\n number(inputKey?: string): ResourceFieldBuilder;\n\n /**\n * Get a boolean field builder\n */\n boolean(inputKey?: string): ResourceFieldBuilder;\n\n /**\n * Get a float field builder\n */\n float(inputKey?: string): ResourceFieldBuilder;\n\n /**\n * Get a int field builder\n */\n int(inputKey?: string): ResourceFieldBuilder;\n}\n\n/**\n * Resource constructor\n */\nexport interface ResourceConstructor<Out = GenericObject> {\n new (originalData: GenericObject | Resource | Model): ResourceContract<Out>;\n}\n\nexport class Resource implements ResourceContract {\n /**\n * Resource data\n */\n public resource: GenericObject = {};\n\n /**\n * Resource final output\n */\n public data: GenericObject = {};\n\n /**\n * Tracks visited object identities during self-reference recursion.\n * Prevents infinite loops on circular data (e.g. A.parent → B, B.parent → A).\n */\n protected _selfSeen?: Set<unknown>;\n\n /**\n * Raw resource schema — field declarations as written by the developer.\n * Used by doc generators for introspection.\n */\n public static schema: Record<string, ResourceFieldConfig | string> = {};\n\n /**\n * Normalized schema — all string cast types and tuples converted to\n * ResourceFieldBuilder instances at definition time. Used by transformOutput at runtime.\n */\n public static parsedSchema: Record<string, ResourceFieldConfig> = {};\n\n /**\n * Normalize a raw schema into parsedSchema.\n * Converts string cast types (including suffixes) and tuples into pre-built builders.\n * Other entry types (ResourceConstructor, resolver functions, ResourceArraySchema) are kept as-is.\n */\n public static normalizeSchema(schema: Record<string, ResourceFieldConfig | string>): Record<string, ResourceFieldConfig> {\n const parsed: Record<string, ResourceFieldConfig> = {};\n\n for (const [key, value] of Object.entries(schema)) {\n if (value === \"self\" || value === \"self[]\") {\n // Keep self-references as-is — resolved lazily in transformValue\n parsed[key] = value;\n } else if (isLazy<ResourceConstructor>(value)) {\n // Keep Lazy<ResourceConstructor> as-is — resolved at transform\n // time so cyclic resource refs (A embeds B embeds A) compose\n // without import-graph cycles.\n parsed[key] = value;\n } else if (typeof value === \"string\") {\n parsed[key] = ResourceFieldBuilder.fromCastType(value);\n } else if (\n Array.isArray(value) &&\n value.length === 2 &&\n typeof value[0] === \"string\" &&\n typeof value[1] === \"string\"\n ) {\n const builder = ResourceFieldBuilder.fromCastType(value[1]);\n builder.setInputKey(value[0]);\n parsed[key] = builder;\n } else if (isArrayOfSchema(value)) {\n // arrayOf item schemas follow the same rules as the top level: cast\n // strings and tuples become builders (nested arrayOf recurses).\n parsed[key] = { ...value, schema: Resource.normalizeSchema(value.schema) };\n } else {\n parsed[key] = value;\n }\n }\n\n return parsed;\n }\n\n /**\n * Constructor\n */\n public constructor(public originalData: GenericObject | Resource | Model) {\n if (this.originalData instanceof Model) {\n this.resource = this.originalData.data;\n } else if (this.originalData instanceof Resource) {\n this.resource = this.originalData.data;\n } else {\n this.resource = this.originalData;\n }\n }\n\n /**\n * Convert resource to JSON\n */\n public toJSON() {\n this.boot();\n this.transformOutput();\n this.extend();\n\n return this.data;\n }\n\n /**\n * Boot method\n * Called before transforming the resource\n */\n protected boot() {\n //\n }\n\n /**\n * Transform resource to output using the pre-normalized parsedSchema.\n * Builders handle their own array/nullable logic internally.\n * ResourceConstructor and ResourceArraySchema handle arrays in transformValue.\n */\n protected transformOutput() {\n const localeCode = useRequestStore()?.request?.locale;\n const parsedSchema = this.resolveParsedSchema();\n // A model is read through its accessors; anything else through the\n // constructor's normalized input (a wrapped resource's serialized data).\n const source = this.originalData instanceof Model ? this.originalData : this.resource;\n\n for (const [outputKey, outputSettings] of Object.entries(parsedSchema)) {\n const inputKey = outputSettings instanceof ResourceFieldBuilder\n ? outputSettings.getInputKey()\n : undefined;\n const inputValue = this.resolveSourceValue(source, outputKey, inputKey);\n const outputValue = this.transformValue(inputValue, outputSettings, localeCode);\n\n if (outputValue !== undefined) {\n this.set(outputKey, outputValue);\n }\n }\n }\n\n /**\n * Resolve the normalized schema of THIS class.\n * `@RegisterResource()` is optional: when the class has no own `parsedSchema`\n * (a static field is inherited from the base as `{}`), normalize its own\n * static `schema` (or an instance `schema` field) once and cache it on the class.\n */\n protected resolveParsedSchema(): Record<string, ResourceFieldConfig> {\n const ctor = this.constructor as typeof Resource;\n\n if (Object.hasOwn(ctor, \"parsedSchema\")) {\n return ctor.parsedSchema;\n }\n\n const instanceSchema = (this as unknown as { schema?: Record<string, ResourceFieldConfig | string> }).schema;\n const schema = instanceSchema ?? ctor.schema;\n\n if (!schema || Object.keys(schema).length === 0) {\n throw new Error(\n `Resource \"${ctor.name}\" has no schema. Declare \\`static schema = { ... }\\` on the class.`,\n );\n }\n\n const parsed = Resource.normalizeSchema(schema);\n\n // Instance schemas are per-class by convention, so caching on the class is safe\n ctor.parsedSchema = parsed;\n\n return parsed;\n }\n\n /**\n * Transform the given value with given type\n */\n public transform(value: any, type: ResourceOutputValueCastType, locale?: string) {\n return new ResourceFieldBuilder(type).transform(value, locale);\n }\n\n /**\n * Transform the given value for the given output setting.\n * After normalization, string cast types no longer reach here — they are pre-converted to builders.\n */\n protected transformValue(value: any, outputSettings: ResourceFieldConfig, locale?: string): any {\n let outputValue: any;\n\n if (isLazy<ResourceConstructor>(outputSettings)) {\n // Resolve the lazy ref now (cyclic resource imports are fully loaded\n // by serialization time) and recurse — the resolved constructor is\n // handled by the ResourceConstructor branch below.\n return this.transformValue(value, outputSettings.resolve(), locale);\n }\n\n if (isNullableResource(outputSettings)) {\n // nullable(Resource) — always present: null for a missing input, the resource output otherwise\n if (value === null || value === undefined) return null;\n\n return this.transformValue(value, outputSettings.resource, locale) ?? null;\n }\n\n if (Array.isArray(outputSettings) && outputSettings.length === 1) {\n // [Resource] — a list field. [] stays [], a missing/null input omits the field.\n if (!Array.isArray(value)) return;\n\n return value\n .map((item) => this.transformValue(item, outputSettings[0], locale))\n .filter((v) => v !== undefined);\n }\n\n if (outputSettings === \"self\" || outputSettings === \"self[]\") {\n // Self-reference — resolve using the same resource class with cycle detection\n outputValue = this.transformSelfReference(value, outputSettings === \"self[]\");\n } else if (\n typeof outputSettings === \"function\" &&\n outputSettings.prototype instanceof Resource\n ) {\n if (!value) return;\n // Nested resource — handle both single and array values\n if (Array.isArray(value)) {\n // An empty list stays an empty list ([] is the honest output); only a\n // missing/null input omits the field.\n outputValue = value\n .map((item) => new (outputSettings as typeof Resource)(item).toJSON())\n .filter((v) => v !== undefined);\n } else {\n outputValue = new (outputSettings as typeof Resource)(value).toJSON();\n }\n } else if (typeof outputSettings === \"function\") {\n // Resolver function - bind to Resource instance for access to this.get(), etc.\n outputValue = (outputSettings as Function).call(this, value, this);\n } else if (outputSettings instanceof ResourceFieldBuilder) {\n // Builder — handles array mapping and nullable internally via isArrayField\n outputValue = outputSettings.transform(value, locale);\n } else if (\n typeof outputSettings === \"object\" &&\n outputSettings !== null &&\n \"__type\" in outputSettings &&\n outputSettings.__type === \"arrayOf\"\n ) {\n // ResourceArraySchema — structured array items with their own sub-schema\n if (Array.isArray(value)) {\n outputValue = value\n .map((item) => this.transformArrayItem(item, outputSettings.schema, locale))\n .filter((v) => v !== undefined);\n } else {\n outputValue = this.transformArrayItem(value, outputSettings.schema, locale);\n }\n }\n\n return outputValue;\n }\n\n /**\n * Extend the resource output\n */\n protected extend() {\n //\n }\n\n /**\n * Transform a self-referencing field value.\n *\n * @example\n * // Single: parent: \"self\"\n * // Array: children: \"self[]\"\n */\n protected transformSelfReference(value: any, isArray: boolean): any {\n if (isArray) {\n return Array.isArray(value)\n ? value.map((item) => this.resolveSelf(item)).filter((v) => v !== undefined)\n : undefined;\n }\n\n return this.resolveSelf(value);\n }\n\n /**\n * Resolve a single self-reference value.\n * Uses identity-based cycle detection (id/_id) and a max depth guard.\n */\n protected resolveSelf(value: any): any {\n if (!value) return undefined;\n\n const identity = value.id ?? value._id ?? value;\n // Ancestors of the current path only: siblings never see each other,\n // so the set size is the depth.\n const ancestors = this._selfSeen ?? new Set();\n\n // Circular reference or depth limit reached — stop recursion\n if (ancestors.has(identity) || ancestors.size >= MAX_SELF_DEPTH) {\n return undefined;\n }\n\n const SelfConstructor = this.constructor as typeof Resource;\n const child = new SelfConstructor(value);\n child._selfSeen = new Set(ancestors).add(identity);\n\n return child.toJSON();\n }\n\n /**\n * Transform a single array item according to the given schema\n */\n protected transformArrayItem(\n item: any,\n schema: Record<string, ResourceFieldConfig>,\n locale?: string,\n ) {\n const transformedItem: GenericObject = {};\n\n for (const [outputKey, outputSettings] of Object.entries(schema)) {\n let fieldKey = outputKey;\n let valueTransformType = outputSettings as ResourceFieldConfig;\n\n if (Array.isArray(outputSettings) && outputSettings.length === 2) {\n fieldKey = outputSettings[0] as string;\n valueTransformType = outputSettings[1] as ResourceFieldConfig;\n }\n\n const inputKey = valueTransformType instanceof ResourceFieldBuilder\n ? valueTransformType.getInputKey()\n : undefined;\n const inputValue = this.resolveSourceValue(item, outputKey, inputKey ?? fieldKey);\n const outputValue = this.transformValue(inputValue, valueTransformType, locale);\n\n if (outputValue !== undefined) {\n set(transformedItem, outputKey, outputValue);\n }\n }\n\n return transformedItem;\n }\n\n /**\n * Resolve one schema field from a model or plain object. Mongo records use `_id`\n * internally, while resources expose `id` by convention.\n */\n protected resolveSourceValue(source: GenericObject | Resource | Model, outputKey: string, inputKey?: string) {\n const key = inputKey ?? outputKey;\n const read = (field: string) => source instanceof Model ? source.get(field) : get(source, field);\n const value = read(key);\n\n return outputKey === \"id\" && key === \"id\" && value === undefined ? read(\"_id\") : value;\n }\n\n /**\n * Get a input value for the given key\n */\n public get(key: string, defaultValue?: any) {\n return get(this.resource, key, defaultValue);\n }\n\n /**\n * Set the given value for the given field\n */\n public set(key: string, value: any): ResourceContract {\n set(this.data, key, value);\n\n return this;\n }\n\n /**\n * Create an array schema for transforming array items\n */\n public arrayOf(schema: Record<string, ResourceFieldConfig>): ResourceArraySchema {\n return {\n __type: \"arrayOf\",\n schema,\n };\n }\n\n /**\n * Get a string field builder\n */\n public string(inputKey?: string) {\n return this.fieldBuilder(\"string\", inputKey);\n }\n\n /**\n * Get a date field builder\n */\n public date(inputKey?: string) {\n return this.fieldBuilder(\"date\", inputKey);\n }\n\n /**\n * Get a localized field builder\n */\n public localized(inputKey?: string) {\n return this.fieldBuilder(\"localized\", inputKey);\n }\n\n /**\n * Get a url field builder\n */\n public url(inputKey?: string) {\n return this.fieldBuilder(\"url\", inputKey);\n }\n\n /**\n * Get a uploadsUrl field builder\n */\n public uploadsUrl(inputKey?: string) {\n return this.fieldBuilder(\"uploadsUrl\", inputKey);\n }\n\n /**\n * Get a number field builder\n */\n public number(inputKey?: string) {\n return this.fieldBuilder(\"number\", inputKey);\n }\n\n /**\n * Get a boolean field builder\n */\n public boolean(inputKey?: string) {\n return this.fieldBuilder(\"boolean\", inputKey);\n }\n\n /**\n * Get a float field builder\n */\n public float(inputKey?: string) {\n return this.fieldBuilder(\"float\", inputKey);\n }\n\n /**\n * Get a int field builder\n */\n public int(inputKey?: string) {\n return this.fieldBuilder(\"int\", inputKey);\n }\n\n /**\n * New field builder\n */\n protected fieldBuilder(type: ResourceOutputValueCastType, inputKey?: string) {\n const builder = new ResourceFieldBuilder(type);\n\n if (inputKey) {\n builder.setInputKey(inputKey);\n }\n\n return builder;\n }\n}\n"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import { ResponseStatus } from "../http/response.mjs";
2
2
  import { ResourceFieldBuilder } from "./resource-field-builder.mjs";
3
3
  import { ResourceConstructor, ResourceContract } from "./resource.mjs";
4
+ import { NullableResource } from "./nullable-resource.mjs";
4
5
  import { Lazy } from "@mongez/reinforcements";
5
6
 
6
7
  //#region ../core/src/resource/types.d.ts
@@ -26,7 +27,7 @@ type ResourceArraySchema = {
26
27
  __type: "arrayOf";
27
28
  schema: Record<string, ResourceFieldConfig>;
28
29
  };
29
- type ResourceFieldConfig = ResourceCastType | ResourceConstructor | ResourceSelfReference | Lazy<ResourceConstructor> | [string, ResourceCastType] | ResourceFieldBuilder | ResourceArraySchema | ((value: any, resource: ResourceContract) => any);
30
+ type ResourceFieldConfig = ResourceCastType | ResourceConstructor | ResourceSelfReference | Lazy<ResourceConstructor> | NullableResource | [ResourceConstructor] | [string, ResourceCastType] | ResourceFieldBuilder | ResourceArraySchema | ((value: any, resource: ResourceContract) => any);
30
31
  type ResourceSchema = Record<string, ResourceFieldConfig>;
31
32
  type ResourceFieldBuilderDateOutputOptions = {
32
33
  /**
@@ -64,9 +65,10 @@ type ResourceFieldBuilderDateOutputOptions = {
64
65
  * (e.g. "string", "number?", "string[]", "date[]?")
65
66
  * - ResourceConstructor for a single nested resource object
66
67
  * - [ResourceConstructor] (tuple) for an array of a nested resource
68
+ * - nullable(ResourceConstructor) for a nested resource that is `null` when the value is missing
67
69
  * - A nested plain object of any of the above
68
70
  */
69
- type ResponseBodyValue = ResourceCastType | ResourceConstructor | [ResourceConstructor] | {
71
+ type ResponseBodyValue = ResourceCastType | ResourceConstructor | [ResourceConstructor] | NullableResource<ResourceConstructor<any>> | {
70
72
  [key: string]: ResponseBodyValue;
71
73
  };
72
74
  /**
@@ -112,7 +114,9 @@ type CastOutput<C extends string> = C extends `${infer B}[]?` ? BaseCastOutput<B
112
114
  * `Root` is the schema of the resource being defined, used to resolve `"self"` and `"self[]"`.
113
115
  * Anything that cannot be inferred statically (builders) is `unknown`, never `any`.
114
116
  */
115
- type FieldOutput<F, Root extends ResourceSchema = ResourceSchema> = F extends "self" ? ResourceOutputOf<Root, Root> : F extends "self[]" ? ResourceOutputOf<Root, Root>[] : F extends string ? CastOutput<F> : F extends readonly [string, infer C extends string] ? CastOutput<C> : F extends ResourceFieldBuilder ? unknown : F extends Lazy<infer R> ? FieldOutput<R, Root> : F extends {
117
+ type FieldOutput<F, Root extends ResourceSchema = ResourceSchema> = F extends "self" ? ResourceOutputOf<Root, Root> : F extends "self[]" ? ResourceOutputOf<Root, Root>[] : F extends string ? CastOutput<F> : F extends NullableResource<infer R> ? FieldOutput<R, Root> | null : F extends readonly [new (...args: any[]) => {
118
+ toJSON(): infer O;
119
+ }] ? AnyToUnknown<O>[] : F extends readonly [string, infer C extends string] ? CastOutput<C> : F extends ResourceFieldBuilder ? unknown : F extends Lazy<infer R> ? FieldOutput<R, Root> : F extends {
116
120
  readonly __type: "arrayOf";
117
121
  readonly schema: infer Item extends ResourceSchema;
118
122
  } ? ResourceOutputOf<Item, Root>[] : F extends (new (...args: any[]) => {
@@ -141,12 +145,13 @@ type ResourceOutput<R> = R extends (new (...args: any[]) => {
141
145
  /**
142
146
  * Output type of one response schema body value, with the same rules as a resource schema:
143
147
  * a cast string resolves through `CastOutput`, a resource through `ResourceOutput`,
144
- * `[Resource]` to an array of it, and a nested object recurses.
148
+ * `[Resource]` to an array of it, `nullable(Resource)` to its output or `null`, and a nested
149
+ * object recurses.
145
150
  *
146
151
  * @example
147
152
  * type Out = ResponseBodyOutput<{ user: typeof UserResource; token: "string"; tags: "string[]?" }>;
148
153
  */
149
- type ResponseBodyOutput<B> = B extends string ? CastOutput<B> : B extends readonly [infer R] ? ResourceOutput<R>[] : B extends (new (...args: any[]) => unknown) ? ResourceOutput<B> : B extends object ? { -readonly [K in keyof B]: ResponseBodyOutput<B[K]> } : never;
154
+ type ResponseBodyOutput<B> = B extends string ? CastOutput<B> : B extends readonly [infer R] ? ResourceOutput<R>[] : B extends (new (...args: any[]) => unknown) ? ResourceOutput<B> : B extends NullableResource<infer R> ? ResourceOutput<R> | null : B extends object ? { -readonly [K in keyof B]: ResponseBodyOutput<B[K]> } : never;
150
155
  /**
151
156
  * Output type of a whole response schema: the body output per declared status code.
152
157
  *
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/resource/types.ts"],"mappings":"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"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/resource/types.ts"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { isNullableResource } from "../resource/nullable-resource.mjs";
1
2
  import { posix, relative, sep } from "node:path";
2
3
 
3
4
  //#region ../core/src/router/response-schema-types.ts
@@ -52,6 +53,7 @@ function describeResource(reference) {
52
53
  }
53
54
  function describeBodyValue(value, path, resolveResource, onUnmapped) {
54
55
  if (typeof value === "string") return describeCast(value);
56
+ if (isNullableResource(value)) return `(${describeBodyValue(value.resource, path, resolveResource, onUnmapped)}) | null`;
55
57
  if (typeof value === "function") {
56
58
  const reference = resolveResource(value);
57
59
  if (!reference) {
@@ -1 +1 @@
1
- {"version":3,"file":"response-schema-types.mjs","names":[],"sources":["../../../../../../../core/src/router/response-schema-types.ts"],"sourcesContent":["import { posix, relative, sep } from \"node:path\";\n\n/**\n * Where a resource class lives: the module it is exported from and the export name.\n */\nexport type ResourceModuleExport = Readonly<{\n file: string;\n exportName: string;\n}>;\n\n/**\n * Import location of a resource as it is written in a generated declaration file.\n * `importPath` has no extension and starts with `./` or `../`.\n */\nexport type ResourceTypeReference = Readonly<{\n importPath: string;\n exportName: string;\n}>;\n\n/**\n * Maps a response-schema resource value back to where it is exported from.\n * Returns `undefined` when the value is not a known resource export.\n */\nexport type ResourceTypeResolver = (value: unknown) => ResourceTypeReference | undefined;\n\n/**\n * A loaded module namespace together with the file it was imported from.\n */\nexport type ResourceModuleNamespace = Readonly<{\n file: string;\n namespace: Record<string, unknown>;\n}>;\n\n/**\n * Called with the dotted path of every field that could not be mapped\n * (for example `200.body.user`).\n */\nexport type UnmappedResourceHandler = (fieldPath: string) => void;\n\nconst CORE_PACKAGE = \"@warlock.js/core\";\n\nfunction isIndexFile(file: string): boolean {\n return /(^|[\\\\/])index\\.[cm]?[jt]sx?$/.test(file);\n}\n\n/**\n * Build an identity map from every function export (resource classes) to the module\n * export it came from.\n *\n * When the same value is exported more than once (a barrel re-export, or a default next\n * to a named export) the most specific location wins: a non-`index.*` file beats an\n * `index.*` file, then a named export beats `default`, then the first one seen.\n */\nexport function buildResourceMap(\n namespaces: readonly ResourceModuleNamespace[],\n): Map<unknown, ResourceModuleExport> {\n const map = new Map<unknown, ResourceModuleExport>();\n\n const rank = (location: ResourceModuleExport): number =>\n (isIndexFile(location.file) ? 2 : 0) + (location.exportName === \"default\" ? 1 : 0);\n\n for (const { file, namespace } of namespaces) {\n for (const [exportName, value] of Object.entries(namespace)) {\n if (typeof value !== \"function\") continue;\n\n const candidate: ResourceModuleExport = { file, exportName };\n const existing = map.get(value);\n\n if (!existing || rank(candidate) < rank(existing)) {\n map.set(value, candidate);\n }\n }\n }\n\n return map;\n}\n\n/**\n * Turn a resource map into a resolver whose import paths are relative to\n * `<appRoot>/.warlock/typings/`, the folder the generated declarations live in.\n */\nexport function createResourceTypeResolver(\n map: ReadonlyMap<unknown, ResourceModuleExport>,\n appRoot: string,\n): ResourceTypeResolver {\n const typingsDirectory = `${appRoot}${sep}.warlock${sep}typings`;\n\n return (value) => {\n const location = map.get(value);\n\n if (!location) return undefined;\n\n let importPath = relative(typingsDirectory, location.file)\n .split(sep)\n .join(posix.sep)\n .replace(/\\.[cm]?[jt]sx?$/, \"\");\n\n if (!importPath.startsWith(\".\")) {\n importPath = `./${importPath}`;\n }\n\n return { importPath, exportName: location.exportName };\n };\n}\n\nfunction describeCast(cast: string): string {\n return `import(${JSON.stringify(CORE_PACKAGE)}).CastOutput<${JSON.stringify(cast)}>`;\n}\n\nfunction describeResource(reference: ResourceTypeReference): string {\n return (\n `import(${JSON.stringify(CORE_PACKAGE)}).ResourceOutput<` +\n `typeof import(${JSON.stringify(reference.importPath)})[${JSON.stringify(reference.exportName)}]>`\n );\n}\n\nfunction describeBodyValue(\n value: unknown,\n path: string,\n resolveResource: ResourceTypeResolver,\n onUnmapped: UnmappedResourceHandler | undefined,\n): string {\n if (typeof value === \"string\") {\n return describeCast(value);\n }\n\n if (typeof value === \"function\") {\n const reference = resolveResource(value);\n\n if (!reference) {\n onUnmapped?.(path);\n return \"unknown\";\n }\n\n return describeResource(reference);\n }\n\n if (Array.isArray(value)) {\n return `(${describeBodyValue(value[0], path, resolveResource, onUnmapped)})[]`;\n }\n\n if (typeof value === \"object\" && value !== null) {\n const members = Object.entries(value).map(\n ([key, member]) =>\n `${JSON.stringify(key)}: ${describeBodyValue(member, `${path}.${key}`, resolveResource, onUnmapped)}`,\n );\n\n return members.length === 0 ? \"{}\" : `{ ${members.join(\"; \")} }`;\n }\n\n return \"unknown\";\n}\n\n/**\n * Serialize a route's `responseSchema` into TypeScript type expressions, one per status code.\n *\n * The expressions are self-contained: they reference only `CastOutput` and `ResourceOutput`\n * from `@warlock.js/core`, `typeof import(...)` of resource modules, object literal types\n * and arrays. A resource that `resolveResource` cannot map becomes `unknown` and is\n * reported through `onUnmapped`.\n *\n * Both the typings child process and the dev server call this, so their output matches.\n *\n * @returns `undefined` when the schema declares no status code.\n *\n * @example\n * describeResponseSchemaTypes({ 400: { body: { error: \"string\" } } }, resolve);\n * // { \"400\": '{ \"error\": import(\"@warlock.js/core\").CastOutput<\"string\"> }' }\n */\nexport function describeResponseSchemaTypes(\n schema: unknown,\n resolveResource: ResourceTypeResolver,\n onUnmapped?: UnmappedResourceHandler,\n): Record<string, string> | undefined {\n if (typeof schema !== \"object\" || schema === null) return undefined;\n\n const described: Record<string, string> = {};\n\n for (const [status, entry] of Object.entries(schema)) {\n if (typeof entry !== \"object\" || entry === null) continue;\n\n const body = (entry as { body?: unknown }).body;\n\n if (typeof body !== \"object\" || body === null) continue;\n\n described[status] = describeBodyValue(body, `${status}.body`, resolveResource, onUnmapped);\n }\n\n return Object.keys(described).length === 0 ? undefined : described;\n}\n"],"mappings":";;;AAuCA,MAAM,eAAe;AAErB,SAAS,YAAY,MAAuB;CAC1C,OAAO,gCAAgC,KAAK,IAAI;AAClD;;;;;;;;;AAUA,SAAgB,iBACd,YACoC;CACpC,MAAM,sBAAM,IAAI,IAAmC;CAEnD,MAAM,QAAQ,cACX,YAAY,SAAS,IAAI,IAAI,IAAI,MAAM,SAAS,eAAe,YAAY,IAAI;CAElF,KAAK,MAAM,EAAE,MAAM,eAAe,YAChC,KAAK,MAAM,CAAC,YAAY,UAAU,OAAO,QAAQ,SAAS,GAAG;EAC3D,IAAI,OAAO,UAAU,YAAY;EAEjC,MAAM,YAAkC;GAAE;GAAM;EAAW;EAC3D,MAAM,WAAW,IAAI,IAAI,KAAK;EAE9B,IAAI,CAAC,YAAY,KAAK,SAAS,IAAI,KAAK,QAAQ,GAC9C,IAAI,IAAI,OAAO,SAAS;CAE5B;CAGF,OAAO;AACT;;;;;AAMA,SAAgB,2BACd,KACA,SACsB;CACtB,MAAM,mBAAmB,GAAG,UAAU,IAAI,UAAU,IAAI;CAExD,QAAQ,UAAU;EAChB,MAAM,WAAW,IAAI,IAAI,KAAK;EAE9B,IAAI,CAAC,UAAU,OAAO;EAEtB,IAAI,aAAa,SAAS,kBAAkB,SAAS,IAAI,CAAC,CACvD,MAAM,GAAG,CAAC,CACV,KAAK,MAAM,GAAG,CAAC,CACf,QAAQ,mBAAmB,EAAE;EAEhC,IAAI,CAAC,WAAW,WAAW,GAAG,GAC5B,aAAa,KAAK;EAGpB,OAAO;GAAE;GAAY,YAAY,SAAS;EAAW;CACvD;AACF;AAEA,SAAS,aAAa,MAAsB;CAC1C,OAAO,UAAU,KAAK,UAAU,YAAY,EAAE,eAAe,KAAK,UAAU,IAAI,EAAE;AACpF;AAEA,SAAS,iBAAiB,WAA0C;CAClE,OACE,UAAU,KAAK,UAAU,YAAY,EAAE,iCACtB,KAAK,UAAU,UAAU,UAAU,EAAE,IAAI,KAAK,UAAU,UAAU,UAAU,EAAE;AAEnG;AAEA,SAAS,kBACP,OACA,MACA,iBACA,YACQ;CACR,IAAI,OAAO,UAAU,UACnB,OAAO,aAAa,KAAK;CAG3B,IAAI,OAAO,UAAU,YAAY;EAC/B,MAAM,YAAY,gBAAgB,KAAK;EAEvC,IAAI,CAAC,WAAW;GACd,aAAa,IAAI;GACjB,OAAO;EACT;EAEA,OAAO,iBAAiB,SAAS;CACnC;CAEA,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,IAAI,kBAAkB,MAAM,IAAI,MAAM,iBAAiB,UAAU,EAAE;CAG5E,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM;EAC/C,MAAM,UAAU,OAAO,QAAQ,KAAK,CAAC,CAAC,KACnC,CAAC,KAAK,YACL,GAAG,KAAK,UAAU,GAAG,EAAE,IAAI,kBAAkB,QAAQ,GAAG,KAAK,GAAG,OAAO,iBAAiB,UAAU,GACtG;EAEA,OAAO,QAAQ,WAAW,IAAI,OAAO,KAAK,QAAQ,KAAK,IAAI,EAAE;CAC/D;CAEA,OAAO;AACT;;;;;;;;;;;;;;;;;AAkBA,SAAgB,4BACd,QACA,iBACA,YACoC;CACpC,IAAI,OAAO,WAAW,YAAY,WAAW,MAAM,OAAO;CAE1D,MAAM,YAAoC,CAAC;CAE3C,KAAK,MAAM,CAAC,QAAQ,UAAU,OAAO,QAAQ,MAAM,GAAG;EACpD,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM;EAEjD,MAAM,OAAQ,MAA6B;EAE3C,IAAI,OAAO,SAAS,YAAY,SAAS,MAAM;EAE/C,UAAU,UAAU,kBAAkB,MAAM,GAAG,OAAO,QAAQ,iBAAiB,UAAU;CAC3F;CAEA,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,WAAW,IAAI,SAAY;AAC3D"}
1
+ {"version":3,"file":"response-schema-types.mjs","names":[],"sources":["../../../../../../../core/src/router/response-schema-types.ts"],"sourcesContent":["import { posix, relative, sep } from \"node:path\";\nimport { isNullableResource } from \"../resource/nullable-resource\";\n\n/**\n * Where a resource class lives: the module it is exported from and the export name.\n */\nexport type ResourceModuleExport = Readonly<{\n file: string;\n exportName: string;\n}>;\n\n/**\n * Import location of a resource as it is written in a generated declaration file.\n * `importPath` has no extension and starts with `./` or `../`.\n */\nexport type ResourceTypeReference = Readonly<{\n importPath: string;\n exportName: string;\n}>;\n\n/**\n * Maps a response-schema resource value back to where it is exported from.\n * Returns `undefined` when the value is not a known resource export.\n */\nexport type ResourceTypeResolver = (value: unknown) => ResourceTypeReference | undefined;\n\n/**\n * A loaded module namespace together with the file it was imported from.\n */\nexport type ResourceModuleNamespace = Readonly<{\n file: string;\n namespace: Record<string, unknown>;\n}>;\n\n/**\n * Called with the dotted path of every field that could not be mapped\n * (for example `200.body.user`).\n */\nexport type UnmappedResourceHandler = (fieldPath: string) => void;\n\nconst CORE_PACKAGE = \"@warlock.js/core\";\n\nfunction isIndexFile(file: string): boolean {\n return /(^|[\\\\/])index\\.[cm]?[jt]sx?$/.test(file);\n}\n\n/**\n * Build an identity map from every function export (resource classes) to the module\n * export it came from.\n *\n * When the same value is exported more than once (a barrel re-export, or a default next\n * to a named export) the most specific location wins: a non-`index.*` file beats an\n * `index.*` file, then a named export beats `default`, then the first one seen.\n */\nexport function buildResourceMap(\n namespaces: readonly ResourceModuleNamespace[],\n): Map<unknown, ResourceModuleExport> {\n const map = new Map<unknown, ResourceModuleExport>();\n\n const rank = (location: ResourceModuleExport): number =>\n (isIndexFile(location.file) ? 2 : 0) + (location.exportName === \"default\" ? 1 : 0);\n\n for (const { file, namespace } of namespaces) {\n for (const [exportName, value] of Object.entries(namespace)) {\n if (typeof value !== \"function\") continue;\n\n const candidate: ResourceModuleExport = { file, exportName };\n const existing = map.get(value);\n\n if (!existing || rank(candidate) < rank(existing)) {\n map.set(value, candidate);\n }\n }\n }\n\n return map;\n}\n\n/**\n * Turn a resource map into a resolver whose import paths are relative to\n * `<appRoot>/.warlock/typings/`, the folder the generated declarations live in.\n */\nexport function createResourceTypeResolver(\n map: ReadonlyMap<unknown, ResourceModuleExport>,\n appRoot: string,\n): ResourceTypeResolver {\n const typingsDirectory = `${appRoot}${sep}.warlock${sep}typings`;\n\n return (value) => {\n const location = map.get(value);\n\n if (!location) return undefined;\n\n let importPath = relative(typingsDirectory, location.file)\n .split(sep)\n .join(posix.sep)\n .replace(/\\.[cm]?[jt]sx?$/, \"\");\n\n if (!importPath.startsWith(\".\")) {\n importPath = `./${importPath}`;\n }\n\n return { importPath, exportName: location.exportName };\n };\n}\n\nfunction describeCast(cast: string): string {\n return `import(${JSON.stringify(CORE_PACKAGE)}).CastOutput<${JSON.stringify(cast)}>`;\n}\n\nfunction describeResource(reference: ResourceTypeReference): string {\n return (\n `import(${JSON.stringify(CORE_PACKAGE)}).ResourceOutput<` +\n `typeof import(${JSON.stringify(reference.importPath)})[${JSON.stringify(reference.exportName)}]>`\n );\n}\n\nfunction describeBodyValue(\n value: unknown,\n path: string,\n resolveResource: ResourceTypeResolver,\n onUnmapped: UnmappedResourceHandler | undefined,\n): string {\n if (typeof value === \"string\") {\n return describeCast(value);\n }\n\n if (isNullableResource(value)) {\n return `(${describeBodyValue(value.resource, path, resolveResource, onUnmapped)}) | null`;\n }\n\n if (typeof value === \"function\") {\n const reference = resolveResource(value);\n\n if (!reference) {\n onUnmapped?.(path);\n return \"unknown\";\n }\n\n return describeResource(reference);\n }\n\n if (Array.isArray(value)) {\n return `(${describeBodyValue(value[0], path, resolveResource, onUnmapped)})[]`;\n }\n\n if (typeof value === \"object\" && value !== null) {\n const members = Object.entries(value).map(\n ([key, member]) =>\n `${JSON.stringify(key)}: ${describeBodyValue(member, `${path}.${key}`, resolveResource, onUnmapped)}`,\n );\n\n return members.length === 0 ? \"{}\" : `{ ${members.join(\"; \")} }`;\n }\n\n return \"unknown\";\n}\n\n/**\n * Serialize a route's `responseSchema` into TypeScript type expressions, one per status code.\n *\n * The expressions are self-contained: they reference only `CastOutput` and `ResourceOutput`\n * from `@warlock.js/core`, `typeof import(...)` of resource modules, object literal types\n * and arrays. A resource that `resolveResource` cannot map becomes `unknown` and is\n * reported through `onUnmapped`.\n *\n * Both the typings child process and the dev server call this, so their output matches.\n *\n * @returns `undefined` when the schema declares no status code.\n *\n * @example\n * describeResponseSchemaTypes({ 400: { body: { error: \"string\" } } }, resolve);\n * // { \"400\": '{ \"error\": import(\"@warlock.js/core\").CastOutput<\"string\"> }' }\n */\nexport function describeResponseSchemaTypes(\n schema: unknown,\n resolveResource: ResourceTypeResolver,\n onUnmapped?: UnmappedResourceHandler,\n): Record<string, string> | undefined {\n if (typeof schema !== \"object\" || schema === null) return undefined;\n\n const described: Record<string, string> = {};\n\n for (const [status, entry] of Object.entries(schema)) {\n if (typeof entry !== \"object\" || entry === null) continue;\n\n const body = (entry as { body?: unknown }).body;\n\n if (typeof body !== \"object\" || body === null) continue;\n\n described[status] = describeBodyValue(body, `${status}.body`, resolveResource, onUnmapped);\n }\n\n return Object.keys(described).length === 0 ? undefined : described;\n}\n"],"mappings":"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"}