@jaypie/fabric 0.1.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 (166) hide show
  1. package/README.md +677 -0
  2. package/dist/cjs/commander/FabricCommander.d.ts +94 -0
  3. package/dist/cjs/commander/createCommanderOptions.d.ts +25 -0
  4. package/dist/cjs/commander/fabricCommand.d.ts +43 -0
  5. package/dist/cjs/commander/index.cjs +1487 -0
  6. package/dist/cjs/commander/index.cjs.map +1 -0
  7. package/dist/cjs/commander/index.d.ts +6 -0
  8. package/dist/cjs/commander/parseCommanderOptions.d.ts +32 -0
  9. package/dist/cjs/commander/registerServiceCommand.d.ts +43 -0
  10. package/dist/cjs/commander/types.d.ts +107 -0
  11. package/dist/cjs/constants.d.ts +12 -0
  12. package/dist/cjs/convert-date.d.ts +47 -0
  13. package/dist/cjs/convert.d.ts +69 -0
  14. package/dist/cjs/data/FabricData.d.ts +42 -0
  15. package/dist/cjs/data/index.cjs +1575 -0
  16. package/dist/cjs/data/index.cjs.map +1 -0
  17. package/dist/cjs/data/index.d.ts +5 -0
  18. package/dist/cjs/data/services/archive.d.ts +8 -0
  19. package/dist/cjs/data/services/create.d.ts +8 -0
  20. package/dist/cjs/data/services/delete.d.ts +8 -0
  21. package/dist/cjs/data/services/execute.d.ts +8 -0
  22. package/dist/cjs/data/services/index.d.ts +7 -0
  23. package/dist/cjs/data/services/list.d.ts +8 -0
  24. package/dist/cjs/data/services/read.d.ts +8 -0
  25. package/dist/cjs/data/services/update.d.ts +8 -0
  26. package/dist/cjs/data/transforms.d.ts +80 -0
  27. package/dist/cjs/data/types.d.ts +190 -0
  28. package/dist/cjs/express/FabricRouter.d.ts +29 -0
  29. package/dist/cjs/express/fabricExpress.d.ts +16 -0
  30. package/dist/cjs/express/index.cjs +505 -0
  31. package/dist/cjs/express/index.cjs.map +1 -0
  32. package/dist/cjs/express/index.d.ts +3 -0
  33. package/dist/cjs/express/types.d.ts +51 -0
  34. package/dist/cjs/helpers/fallback.d.ts +21 -0
  35. package/dist/cjs/helpers/index.d.ts +3 -0
  36. package/dist/cjs/helpers/resolvedName.d.ts +24 -0
  37. package/dist/cjs/http/FabricHttpServer.d.ts +31 -0
  38. package/dist/cjs/http/authorization.d.ts +30 -0
  39. package/dist/cjs/http/cors.d.ts +40 -0
  40. package/dist/cjs/http/fabricHttp.d.ts +28 -0
  41. package/dist/cjs/http/httpTransform.d.ts +36 -0
  42. package/dist/cjs/http/index.cjs +1820 -0
  43. package/dist/cjs/http/index.cjs.map +1 -0
  44. package/dist/cjs/http/index.d.ts +10 -0
  45. package/dist/cjs/http/stream.d.ts +185 -0
  46. package/dist/cjs/http/types.d.ts +343 -0
  47. package/dist/cjs/index/index.d.ts +8 -0
  48. package/dist/cjs/index/keyBuilder.d.ts +81 -0
  49. package/dist/cjs/index/registry.d.ts +56 -0
  50. package/dist/cjs/index/types.d.ts +54 -0
  51. package/dist/cjs/index.cjs +1674 -0
  52. package/dist/cjs/index.cjs.map +1 -0
  53. package/dist/cjs/index.d.ts +18 -0
  54. package/dist/cjs/lambda/createLambdaService.d.ts +33 -0
  55. package/dist/cjs/lambda/fabricLambda.d.ts +36 -0
  56. package/dist/cjs/lambda/index.cjs +967 -0
  57. package/dist/cjs/lambda/index.cjs.map +1 -0
  58. package/dist/cjs/lambda/index.d.ts +2 -0
  59. package/dist/cjs/lambda/types.d.ts +68 -0
  60. package/dist/cjs/llm/createLlmTool.d.ts +40 -0
  61. package/dist/cjs/llm/fabricTool.d.ts +40 -0
  62. package/dist/cjs/llm/index.cjs +1107 -0
  63. package/dist/cjs/llm/index.cjs.map +1 -0
  64. package/dist/cjs/llm/index.d.ts +3 -0
  65. package/dist/cjs/llm/inputToJsonSchema.d.ts +32 -0
  66. package/dist/cjs/llm/types.d.ts +61 -0
  67. package/dist/cjs/mcp/fabricMcp.d.ts +38 -0
  68. package/dist/cjs/mcp/index.cjs +938 -0
  69. package/dist/cjs/mcp/index.cjs.map +1 -0
  70. package/dist/cjs/mcp/index.d.ts +2 -0
  71. package/dist/cjs/mcp/registerMcpTool.d.ts +38 -0
  72. package/dist/cjs/mcp/types.d.ts +60 -0
  73. package/dist/cjs/models/base.d.ts +209 -0
  74. package/dist/cjs/resolve-date.d.ts +47 -0
  75. package/dist/cjs/resolve.d.ts +69 -0
  76. package/dist/cjs/resolveService.d.ts +49 -0
  77. package/dist/cjs/service.d.ts +13 -0
  78. package/dist/cjs/status.d.ts +30 -0
  79. package/dist/cjs/types/elementaryTypes.d.ts +84 -0
  80. package/dist/cjs/types/fieldCategory.d.ts +20 -0
  81. package/dist/cjs/types/fieldDefinition.d.ts +46 -0
  82. package/dist/cjs/types/index.d.ts +4 -0
  83. package/dist/cjs/types.d.ts +56 -0
  84. package/dist/esm/commander/FabricCommander.d.ts +94 -0
  85. package/dist/esm/commander/createCommanderOptions.d.ts +25 -0
  86. package/dist/esm/commander/fabricCommand.d.ts +43 -0
  87. package/dist/esm/commander/index.d.ts +6 -0
  88. package/dist/esm/commander/index.js +1482 -0
  89. package/dist/esm/commander/index.js.map +1 -0
  90. package/dist/esm/commander/parseCommanderOptions.d.ts +32 -0
  91. package/dist/esm/commander/registerServiceCommand.d.ts +43 -0
  92. package/dist/esm/commander/types.d.ts +107 -0
  93. package/dist/esm/constants.d.ts +12 -0
  94. package/dist/esm/convert-date.d.ts +47 -0
  95. package/dist/esm/convert.d.ts +69 -0
  96. package/dist/esm/data/FabricData.d.ts +42 -0
  97. package/dist/esm/data/index.d.ts +5 -0
  98. package/dist/esm/data/index.js +1548 -0
  99. package/dist/esm/data/index.js.map +1 -0
  100. package/dist/esm/data/services/archive.d.ts +8 -0
  101. package/dist/esm/data/services/create.d.ts +8 -0
  102. package/dist/esm/data/services/delete.d.ts +8 -0
  103. package/dist/esm/data/services/execute.d.ts +8 -0
  104. package/dist/esm/data/services/index.d.ts +7 -0
  105. package/dist/esm/data/services/list.d.ts +8 -0
  106. package/dist/esm/data/services/read.d.ts +8 -0
  107. package/dist/esm/data/services/update.d.ts +8 -0
  108. package/dist/esm/data/transforms.d.ts +80 -0
  109. package/dist/esm/data/types.d.ts +190 -0
  110. package/dist/esm/express/FabricRouter.d.ts +29 -0
  111. package/dist/esm/express/fabricExpress.d.ts +16 -0
  112. package/dist/esm/express/index.d.ts +3 -0
  113. package/dist/esm/express/index.js +500 -0
  114. package/dist/esm/express/index.js.map +1 -0
  115. package/dist/esm/express/types.d.ts +51 -0
  116. package/dist/esm/helpers/fallback.d.ts +21 -0
  117. package/dist/esm/helpers/index.d.ts +3 -0
  118. package/dist/esm/helpers/resolvedName.d.ts +24 -0
  119. package/dist/esm/http/FabricHttpServer.d.ts +31 -0
  120. package/dist/esm/http/authorization.d.ts +30 -0
  121. package/dist/esm/http/cors.d.ts +40 -0
  122. package/dist/esm/http/fabricHttp.d.ts +28 -0
  123. package/dist/esm/http/httpTransform.d.ts +36 -0
  124. package/dist/esm/http/index.d.ts +10 -0
  125. package/dist/esm/http/index.js +1775 -0
  126. package/dist/esm/http/index.js.map +1 -0
  127. package/dist/esm/http/stream.d.ts +185 -0
  128. package/dist/esm/http/types.d.ts +343 -0
  129. package/dist/esm/index/index.d.ts +8 -0
  130. package/dist/esm/index/keyBuilder.d.ts +81 -0
  131. package/dist/esm/index/registry.d.ts +56 -0
  132. package/dist/esm/index/types.d.ts +54 -0
  133. package/dist/esm/index.d.ts +18 -0
  134. package/dist/esm/index.js +1606 -0
  135. package/dist/esm/index.js.map +1 -0
  136. package/dist/esm/lambda/createLambdaService.d.ts +33 -0
  137. package/dist/esm/lambda/fabricLambda.d.ts +36 -0
  138. package/dist/esm/lambda/index.d.ts +2 -0
  139. package/dist/esm/lambda/index.js +965 -0
  140. package/dist/esm/lambda/index.js.map +1 -0
  141. package/dist/esm/lambda/types.d.ts +68 -0
  142. package/dist/esm/llm/createLlmTool.d.ts +40 -0
  143. package/dist/esm/llm/fabricTool.d.ts +40 -0
  144. package/dist/esm/llm/index.d.ts +3 -0
  145. package/dist/esm/llm/index.js +1104 -0
  146. package/dist/esm/llm/index.js.map +1 -0
  147. package/dist/esm/llm/inputToJsonSchema.d.ts +32 -0
  148. package/dist/esm/llm/types.d.ts +61 -0
  149. package/dist/esm/mcp/fabricMcp.d.ts +38 -0
  150. package/dist/esm/mcp/index.d.ts +2 -0
  151. package/dist/esm/mcp/index.js +936 -0
  152. package/dist/esm/mcp/index.js.map +1 -0
  153. package/dist/esm/mcp/registerMcpTool.d.ts +38 -0
  154. package/dist/esm/mcp/types.d.ts +60 -0
  155. package/dist/esm/models/base.d.ts +209 -0
  156. package/dist/esm/resolve-date.d.ts +47 -0
  157. package/dist/esm/resolve.d.ts +69 -0
  158. package/dist/esm/resolveService.d.ts +49 -0
  159. package/dist/esm/service.d.ts +13 -0
  160. package/dist/esm/status.d.ts +30 -0
  161. package/dist/esm/types/elementaryTypes.d.ts +84 -0
  162. package/dist/esm/types/fieldCategory.d.ts +20 -0
  163. package/dist/esm/types/fieldDefinition.d.ts +46 -0
  164. package/dist/esm/types/index.d.ts +4 -0
  165. package/dist/esm/types.d.ts +56 -0
  166. package/package.json +122 -0
@@ -0,0 +1,1606 @@
1
+ import { BadRequestError, ConfigurationError } from '@jaypie/errors';
2
+
3
+ /**
4
+ * FabricModel - Core model vocabulary for @jaypie/fabric
5
+ *
6
+ * Defines the standard fields and structure for all fabric models.
7
+ * Provides a consistent, reusable vocabulary across applications.
8
+ */
9
+ // =============================================================================
10
+ // Field Name Constants
11
+ // =============================================================================
12
+ /**
13
+ * FabricModel field names as constants
14
+ * Useful for building queries, validation, and serialization
15
+ */
16
+ const FABRIC_MODEL_FIELDS = {
17
+ // Content
18
+ CONTENT: "content",
19
+ METADATA: "metadata",
20
+ // Display
21
+ EMOJI: "emoji",
22
+ ICON: "icon",
23
+ // History
24
+ HISTORY: "history",
25
+ // Identity (optional)
26
+ ABBREVIATION: "abbreviation",
27
+ ALIAS: "alias",
28
+ DESCRIPTION: "description",
29
+ LABEL: "label",
30
+ NAME: "name",
31
+ XID: "xid",
32
+ // Identity (required)
33
+ ID: "id",
34
+ // Schema
35
+ CLASS: "class",
36
+ MODEL: "model",
37
+ TYPE: "type",
38
+ // Storage
39
+ SCOPE: "scope",
40
+ SEQUENCE: "sequence",
41
+ // Time
42
+ ARCHIVED_AT: "archivedAt",
43
+ CREATED_AT: "createdAt",
44
+ DELETED_AT: "deletedAt",
45
+ UPDATED_AT: "updatedAt",
46
+ };
47
+ /**
48
+ * Required fields for FabricModel
49
+ */
50
+ const FABRIC_MODEL_REQUIRED_FIELDS = [
51
+ FABRIC_MODEL_FIELDS.CREATED_AT,
52
+ FABRIC_MODEL_FIELDS.ID,
53
+ FABRIC_MODEL_FIELDS.MODEL,
54
+ FABRIC_MODEL_FIELDS.UPDATED_AT,
55
+ ];
56
+ /**
57
+ * Auto-generated fields (set by store, not by user)
58
+ */
59
+ const FABRIC_MODEL_AUTO_FIELDS = [
60
+ FABRIC_MODEL_FIELDS.CREATED_AT,
61
+ FABRIC_MODEL_FIELDS.HISTORY,
62
+ FABRIC_MODEL_FIELDS.ID,
63
+ FABRIC_MODEL_FIELDS.UPDATED_AT,
64
+ ];
65
+ /**
66
+ * Timestamp fields
67
+ */
68
+ const FABRIC_MODEL_TIMESTAMP_FIELDS = [
69
+ FABRIC_MODEL_FIELDS.ARCHIVED_AT,
70
+ FABRIC_MODEL_FIELDS.CREATED_AT,
71
+ FABRIC_MODEL_FIELDS.DELETED_AT,
72
+ FABRIC_MODEL_FIELDS.UPDATED_AT,
73
+ ];
74
+ // =============================================================================
75
+ // Type Guards
76
+ // =============================================================================
77
+ /**
78
+ * Check if a value is a FabricModel
79
+ */
80
+ function isFabricModel(value) {
81
+ if (typeof value !== "object" || value === null) {
82
+ return false;
83
+ }
84
+ const obj = value;
85
+ return (obj.createdAt instanceof Date &&
86
+ typeof obj.id === "string" &&
87
+ typeof obj.model === "string" &&
88
+ obj.updatedAt instanceof Date);
89
+ }
90
+ /**
91
+ * Check if a value has the minimum FabricModel shape (for partial objects)
92
+ */
93
+ function hasFabricModelShape(value) {
94
+ if (typeof value !== "object" || value === null) {
95
+ return false;
96
+ }
97
+ const obj = value;
98
+ return typeof obj.id === "string" && typeof obj.model === "string";
99
+ }
100
+ // =============================================================================
101
+ // Utility Functions
102
+ // =============================================================================
103
+ /**
104
+ * Create a minimal FabricModelInput with required fields
105
+ */
106
+ function createFabricModelInput(overrides) {
107
+ return {
108
+ ...overrides,
109
+ };
110
+ }
111
+ /**
112
+ * Extract only FabricModel fields from an object
113
+ * Useful for sanitizing input or preparing for storage
114
+ */
115
+ function pickFabricModelFields(obj) {
116
+ const fields = Object.values(FABRIC_MODEL_FIELDS);
117
+ const result = {};
118
+ for (const field of fields) {
119
+ if (field in obj) {
120
+ result[field] = obj[field];
121
+ }
122
+ }
123
+ return result;
124
+ }
125
+ /**
126
+ * Check if a field name is a timestamp field
127
+ */
128
+ function isTimestampField(field) {
129
+ return FABRIC_MODEL_TIMESTAMP_FIELDS.includes(field);
130
+ }
131
+ /**
132
+ * Check if a field name is auto-generated
133
+ */
134
+ function isAutoField(field) {
135
+ return FABRIC_MODEL_AUTO_FIELDS.includes(field);
136
+ }
137
+
138
+ /**
139
+ * Meta-modeling Constants
140
+ */
141
+ // =============================================================================
142
+ // Constants
143
+ // =============================================================================
144
+ /** Root organizational unit */
145
+ const APEX = "@";
146
+ /** Fabric version - used to identify pre-instantiated Services */
147
+ const FABRIC_VERSION = "0.1.0";
148
+ /** Composite key separator */
149
+ const SEPARATOR = "#";
150
+ /** System-level models that describe other models */
151
+ const SYSTEM_MODELS = ["context", "field", "model"];
152
+
153
+ /**
154
+ * Date Type Conversion for @jaypie/fabric
155
+ *
156
+ * Adds Date as a supported type in the fabric type system.
157
+ * Follows the same conversion patterns as String, Number, Boolean.
158
+ */
159
+ /**
160
+ * Check if a value is a valid Date
161
+ */
162
+ function isValidDate(value) {
163
+ return value instanceof Date && !Number.isNaN(value.getTime());
164
+ }
165
+ /**
166
+ * Convert a value to a Date
167
+ *
168
+ * Supported inputs:
169
+ * - Date: returned as-is (validated)
170
+ * - Number: treated as Unix timestamp (milliseconds)
171
+ * - String: parsed via Date constructor (ISO 8601, etc.)
172
+ * - Object with value property: unwrapped and converted
173
+ *
174
+ * @throws BadRequestError if value cannot be converted to valid Date
175
+ */
176
+ function fabricDate(value) {
177
+ // Already a Date
178
+ if (value instanceof Date) {
179
+ if (Number.isNaN(value.getTime())) {
180
+ throw new BadRequestError("Invalid Date value");
181
+ }
182
+ return value;
183
+ }
184
+ // Null/undefined
185
+ if (value === null || value === undefined) {
186
+ throw new BadRequestError("Cannot convert null or undefined to Date");
187
+ }
188
+ // Object with value property (fabric pattern)
189
+ if (typeof value === "object" && value !== null && "value" in value) {
190
+ return fabricDate(value.value);
191
+ }
192
+ // Number (timestamp in milliseconds)
193
+ if (typeof value === "number") {
194
+ if (Number.isNaN(value)) {
195
+ throw new BadRequestError("Cannot convert NaN to Date");
196
+ }
197
+ const date = new Date(value);
198
+ if (Number.isNaN(date.getTime())) {
199
+ throw new BadRequestError(`Cannot convert ${value} to Date`);
200
+ }
201
+ return date;
202
+ }
203
+ // String (ISO 8601 or parseable format)
204
+ if (typeof value === "string") {
205
+ // Empty string is invalid
206
+ if (value.trim() === "") {
207
+ throw new BadRequestError("Cannot convert empty string to Date");
208
+ }
209
+ const date = new Date(value);
210
+ if (Number.isNaN(date.getTime())) {
211
+ throw new BadRequestError(`Cannot convert "${value}" to Date`);
212
+ }
213
+ return date;
214
+ }
215
+ // Boolean cannot be converted to Date
216
+ if (typeof value === "boolean") {
217
+ throw new BadRequestError("Cannot convert boolean to Date");
218
+ }
219
+ // Arrays - attempt single element extraction
220
+ if (Array.isArray(value)) {
221
+ if (value.length === 1) {
222
+ return fabricDate(value[0]);
223
+ }
224
+ throw new BadRequestError(`Cannot convert array with ${value.length} elements to Date`);
225
+ }
226
+ throw new BadRequestError(`Cannot convert ${typeof value} to Date`);
227
+ }
228
+ /**
229
+ * Resolve a value from a Date to another type
230
+ *
231
+ * @param value - Date to convert
232
+ * @param targetType - Target type (String, Number)
233
+ */
234
+ function resolveFromDate(value, targetType) {
235
+ if (!isValidDate(value)) {
236
+ throw new BadRequestError("Invalid Date value");
237
+ }
238
+ if (targetType === String) {
239
+ return value.toISOString();
240
+ }
241
+ if (targetType === Number) {
242
+ return value.getTime();
243
+ }
244
+ throw new BadRequestError(`Cannot convert Date to ${targetType}`);
245
+ }
246
+ /**
247
+ * Date type constant for use in fabricService input definitions
248
+ *
249
+ * Usage:
250
+ * ```typescript
251
+ * const handler = fabricService({
252
+ * input: {
253
+ * startDate: { type: Date },
254
+ * endDate: { type: Date, default: undefined }
255
+ * }
256
+ * });
257
+ * ```
258
+ */
259
+ const DateType = Date;
260
+ /**
261
+ * Type guard for Date type in schema definitions
262
+ */
263
+ function isDateType(type) {
264
+ return type === Date;
265
+ }
266
+
267
+ // Fabric functions for @jaypie/fabric
268
+ /**
269
+ * Try to parse a string as JSON if it looks like JSON
270
+ * Returns the parsed value or the original string if not JSON
271
+ */
272
+ function tryParseJson(value) {
273
+ const trimmed = value.trim();
274
+ if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
275
+ (trimmed.startsWith("[") && trimmed.endsWith("]"))) {
276
+ try {
277
+ return JSON.parse(trimmed);
278
+ }
279
+ catch {
280
+ // Not valid JSON, return original
281
+ return value;
282
+ }
283
+ }
284
+ return value;
285
+ }
286
+ /**
287
+ * Unwrap arrays and objects to get to the scalar value
288
+ * - Single-element arrays unwrap to their element
289
+ * - Objects with value property unwrap to that value
290
+ * - Recursively unwraps nested structures
291
+ */
292
+ function unwrapToScalar(value) {
293
+ if (value === undefined || value === null) {
294
+ return value;
295
+ }
296
+ // Unwrap single-element arrays
297
+ if (Array.isArray(value)) {
298
+ if (value.length === 0) {
299
+ return undefined;
300
+ }
301
+ if (value.length === 1) {
302
+ return unwrapToScalar(value[0]);
303
+ }
304
+ throw new BadRequestError("Cannot convert multi-value array to scalar");
305
+ }
306
+ // Unwrap objects with value property
307
+ if (typeof value === "object") {
308
+ const obj = value;
309
+ if ("value" in obj) {
310
+ return unwrapToScalar(obj.value);
311
+ }
312
+ throw new BadRequestError("Object must have a value attribute");
313
+ }
314
+ return value;
315
+ }
316
+ /**
317
+ * Prepare a value for scalar conversion by parsing JSON strings and unwrapping
318
+ */
319
+ function prepareForScalarConversion(value) {
320
+ if (value === undefined || value === null) {
321
+ return value;
322
+ }
323
+ // Try to parse JSON strings
324
+ if (typeof value === "string") {
325
+ const parsed = tryParseJson(value);
326
+ if (parsed !== value) {
327
+ // Successfully parsed, unwrap the result
328
+ return unwrapToScalar(parsed);
329
+ }
330
+ return value;
331
+ }
332
+ // Unwrap arrays and objects
333
+ if (Array.isArray(value) || typeof value === "object") {
334
+ return unwrapToScalar(value);
335
+ }
336
+ return value;
337
+ }
338
+ /**
339
+ * Convert a value to a boolean
340
+ * - Arrays, objects, and JSON strings are unwrapped first
341
+ * - String "true" becomes true
342
+ * - String "false" becomes false
343
+ * - Strings that parse to numbers: positive = true, zero or negative = false
344
+ * - Numbers: positive = true, zero or negative = false
345
+ * - Boolean passes through
346
+ */
347
+ function fabricBoolean(value) {
348
+ // Prepare value by parsing JSON and unwrapping arrays/objects
349
+ const prepared = prepareForScalarConversion(value);
350
+ if (prepared === undefined || prepared === null) {
351
+ return undefined;
352
+ }
353
+ if (typeof prepared === "boolean") {
354
+ return prepared;
355
+ }
356
+ if (typeof prepared === "string") {
357
+ if (prepared === "") {
358
+ return undefined;
359
+ }
360
+ const lower = prepared.toLowerCase();
361
+ if (lower === "true") {
362
+ return true;
363
+ }
364
+ if (lower === "false") {
365
+ return false;
366
+ }
367
+ // Try to parse as number
368
+ const num = parseFloat(prepared);
369
+ if (isNaN(num)) {
370
+ throw new BadRequestError(`Cannot convert "${prepared}" to Boolean`);
371
+ }
372
+ return num > 0;
373
+ }
374
+ if (typeof prepared === "number") {
375
+ if (isNaN(prepared)) {
376
+ throw new BadRequestError("Cannot convert NaN to Boolean");
377
+ }
378
+ return prepared > 0;
379
+ }
380
+ throw new BadRequestError(`Cannot convert ${typeof prepared} to Boolean`);
381
+ }
382
+ /**
383
+ * Convert a value to a number
384
+ * - Arrays, objects, and JSON strings are unwrapped first
385
+ * - String "" becomes undefined
386
+ * - String "true" becomes 1
387
+ * - String "false" becomes 0
388
+ * - Strings that parse to numbers use those values
389
+ * - Strings that parse to NaN throw BadRequestError
390
+ * - Boolean true becomes 1, false becomes 0
391
+ * - Number passes through
392
+ */
393
+ function fabricNumber(value) {
394
+ // Prepare value by parsing JSON and unwrapping arrays/objects
395
+ const prepared = prepareForScalarConversion(value);
396
+ if (prepared === undefined || prepared === null) {
397
+ return undefined;
398
+ }
399
+ if (typeof prepared === "number") {
400
+ if (isNaN(prepared)) {
401
+ throw new BadRequestError("Cannot convert NaN to Number");
402
+ }
403
+ return prepared;
404
+ }
405
+ if (typeof prepared === "boolean") {
406
+ return prepared ? 1 : 0;
407
+ }
408
+ if (typeof prepared === "string") {
409
+ if (prepared === "") {
410
+ return undefined;
411
+ }
412
+ const lower = prepared.toLowerCase();
413
+ if (lower === "true") {
414
+ return 1;
415
+ }
416
+ if (lower === "false") {
417
+ return 0;
418
+ }
419
+ const num = parseFloat(prepared);
420
+ if (isNaN(num)) {
421
+ throw new BadRequestError(`Cannot convert "${prepared}" to Number`);
422
+ }
423
+ return num;
424
+ }
425
+ throw new BadRequestError(`Cannot convert ${typeof prepared} to Number`);
426
+ }
427
+ /**
428
+ * Convert a value to a string
429
+ * - Arrays, objects, and JSON strings are unwrapped first
430
+ * - String "" becomes undefined
431
+ * - Boolean true becomes "true", false becomes "false"
432
+ * - Number converts to string representation
433
+ * - String passes through
434
+ */
435
+ function fabricString(value) {
436
+ // Prepare value by parsing JSON and unwrapping arrays/objects
437
+ const prepared = prepareForScalarConversion(value);
438
+ if (prepared === undefined || prepared === null) {
439
+ return undefined;
440
+ }
441
+ if (typeof prepared === "string") {
442
+ if (prepared === "") {
443
+ return undefined;
444
+ }
445
+ return prepared;
446
+ }
447
+ if (typeof prepared === "boolean") {
448
+ return prepared ? "true" : "false";
449
+ }
450
+ if (typeof prepared === "number") {
451
+ if (isNaN(prepared)) {
452
+ throw new BadRequestError("Cannot convert NaN to String");
453
+ }
454
+ return String(prepared);
455
+ }
456
+ throw new BadRequestError(`Cannot convert ${typeof prepared} to String`);
457
+ }
458
+ /**
459
+ * Convert a value to an array
460
+ * - Non-arrays become arrays containing that value
461
+ * - Arrays of a single value become that value (unwrapped)
462
+ * - Multi-value arrays throw BadRequestError
463
+ * - undefined/null become undefined
464
+ */
465
+ function fabricArray(value) {
466
+ if (value === undefined || value === null) {
467
+ return undefined;
468
+ }
469
+ if (Array.isArray(value)) {
470
+ // Arrays pass through (single-element unwrapping happens when converting FROM array)
471
+ return value;
472
+ }
473
+ // Non-arrays become single-element arrays
474
+ return [value];
475
+ }
476
+ /**
477
+ * Resolve a value from an array to a scalar
478
+ * - Single-element arrays become that element
479
+ * - Multi-element arrays throw BadRequestError
480
+ * - Non-arrays pass through
481
+ */
482
+ function resolveFromArray(value) {
483
+ if (value === undefined || value === null) {
484
+ return undefined;
485
+ }
486
+ if (Array.isArray(value)) {
487
+ if (value.length === 0) {
488
+ return undefined;
489
+ }
490
+ if (value.length === 1) {
491
+ return value[0];
492
+ }
493
+ throw new BadRequestError("Cannot convert multi-value array to scalar");
494
+ }
495
+ return value;
496
+ }
497
+ /**
498
+ * Convert a value to an object with a value property
499
+ * - Scalars become { value: scalar }
500
+ * - Arrays become { value: array }
501
+ * - Objects with a value attribute pass through
502
+ * - Objects without a value attribute throw BadRequestError
503
+ * - undefined/null become undefined
504
+ */
505
+ function fabricObject(value) {
506
+ if (value === undefined || value === null) {
507
+ return undefined;
508
+ }
509
+ // Check if already an object (but not an array)
510
+ if (typeof value === "object" && !Array.isArray(value)) {
511
+ const obj = value;
512
+ if ("value" in obj) {
513
+ return obj;
514
+ }
515
+ throw new BadRequestError("Object must have a value attribute");
516
+ }
517
+ // Scalars and arrays become { value: ... }
518
+ return { value };
519
+ }
520
+ /**
521
+ * Resolve a value from an object to its value property
522
+ * - Objects with a value property return that value
523
+ * - Objects without a value throw BadRequestError
524
+ * - Scalars pass through
525
+ */
526
+ function resolveFromObject(value) {
527
+ if (value === undefined || value === null) {
528
+ return undefined;
529
+ }
530
+ if (typeof value === "object" && !Array.isArray(value)) {
531
+ const obj = value;
532
+ if ("value" in obj) {
533
+ return obj.value;
534
+ }
535
+ throw new BadRequestError("Object must have a value attribute");
536
+ }
537
+ return value;
538
+ }
539
+ /**
540
+ * Check if a type is a typed array (e.g., [String], [Number], [], etc.)
541
+ */
542
+ function isTypedArrayType(type) {
543
+ return Array.isArray(type);
544
+ }
545
+ /**
546
+ * Split a string on comma or tab delimiters for typed array conversion.
547
+ * Only splits if the string contains commas or tabs.
548
+ * Returns the original value if not a string or no delimiters found.
549
+ */
550
+ function splitStringForArray(value) {
551
+ if (typeof value !== "string") {
552
+ return value;
553
+ }
554
+ // Check for comma or tab delimiters
555
+ if (value.includes(",")) {
556
+ return value.split(",").map((s) => s.trim());
557
+ }
558
+ if (value.includes("\t")) {
559
+ return value.split("\t").map((s) => s.trim());
560
+ }
561
+ return value;
562
+ }
563
+ /**
564
+ * Try to parse a string as JSON for array context.
565
+ * Returns parsed value if it's an array, otherwise returns original.
566
+ */
567
+ function tryParseJsonArray(value) {
568
+ if (typeof value !== "string") {
569
+ return value;
570
+ }
571
+ const trimmed = value.trim();
572
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
573
+ try {
574
+ const parsed = JSON.parse(trimmed);
575
+ if (Array.isArray(parsed)) {
576
+ return parsed;
577
+ }
578
+ }
579
+ catch {
580
+ // Not valid JSON, fall through
581
+ }
582
+ }
583
+ return value;
584
+ }
585
+ /**
586
+ * Get the element type from a typed array type
587
+ * Returns undefined for untyped arrays ([])
588
+ */
589
+ function getArrayElementType(type) {
590
+ if (type.length === 0) {
591
+ return undefined; // Untyped array
592
+ }
593
+ const elementType = type[0];
594
+ // Handle constructor types
595
+ if (elementType === Boolean)
596
+ return "boolean";
597
+ if (elementType === Number)
598
+ return "number";
599
+ if (elementType === String)
600
+ return "string";
601
+ if (elementType === Object)
602
+ return "object";
603
+ // Handle string types
604
+ if (elementType === "boolean")
605
+ return "boolean";
606
+ if (elementType === "number")
607
+ return "number";
608
+ if (elementType === "string")
609
+ return "string";
610
+ if (elementType === "object")
611
+ return "object";
612
+ // Handle shorthand types
613
+ if (elementType === "")
614
+ return "string"; // "" shorthand for String
615
+ if (typeof elementType === "object" &&
616
+ elementType !== null &&
617
+ Object.keys(elementType).length === 0) {
618
+ return "object"; // {} shorthand for Object
619
+ }
620
+ throw new BadRequestError(`Unknown array element type: ${String(elementType)}`);
621
+ }
622
+ /**
623
+ * Convert a value to a typed array
624
+ * - Tries to parse JSON arrays first
625
+ * - Splits strings on comma/tab if present
626
+ * - Wraps non-arrays in an array
627
+ * - Converts each element to the specified element type
628
+ */
629
+ function fabricTypedArray(value, elementType) {
630
+ // Try to parse JSON array first
631
+ let processed = tryParseJsonArray(value);
632
+ // If still a string, try to split on comma/tab
633
+ processed = splitStringForArray(processed);
634
+ // Convert to array (wraps non-arrays)
635
+ const array = fabricArray(processed);
636
+ if (array === undefined) {
637
+ return undefined;
638
+ }
639
+ // If no element type specified, return as-is
640
+ if (elementType === undefined) {
641
+ return array;
642
+ }
643
+ // Convert each element to the element type
644
+ return array.map((element, index) => {
645
+ try {
646
+ switch (elementType) {
647
+ case "boolean":
648
+ return fabricBoolean(element);
649
+ case "number":
650
+ return fabricNumber(element);
651
+ case "object":
652
+ return fabricObject(element);
653
+ case "string":
654
+ return fabricString(element);
655
+ default:
656
+ throw new BadRequestError(`Unknown element type: ${elementType}`);
657
+ }
658
+ }
659
+ catch (error) {
660
+ if (error instanceof BadRequestError) {
661
+ throw new BadRequestError(`Cannot convert array element at index ${index}: ${error.message}`);
662
+ }
663
+ throw error;
664
+ }
665
+ });
666
+ }
667
+ /**
668
+ * Fabric a value to the specified type
669
+ */
670
+ function fabric(value, type) {
671
+ // Check for Date type first
672
+ if (isDateType(type)) {
673
+ return fabricDate(value);
674
+ }
675
+ // Check for typed array types
676
+ if (isTypedArrayType(type)) {
677
+ const elementType = getArrayElementType(type);
678
+ return fabricTypedArray(value, elementType);
679
+ }
680
+ const normalizedType = normalizeType(type);
681
+ switch (normalizedType) {
682
+ case "array":
683
+ return fabricArray(value);
684
+ case "boolean":
685
+ return fabricBoolean(value);
686
+ case "number":
687
+ return fabricNumber(value);
688
+ case "object":
689
+ return fabricObject(value);
690
+ case "string":
691
+ return fabricString(value);
692
+ default:
693
+ throw new BadRequestError(`Unknown type: ${String(type)}`);
694
+ }
695
+ }
696
+ /**
697
+ * Normalize type to string representation
698
+ */
699
+ function normalizeType(type) {
700
+ if (type === Array || type === "array") {
701
+ return "array";
702
+ }
703
+ if (type === Boolean || type === "boolean") {
704
+ return "boolean";
705
+ }
706
+ if (type === Number || type === "number") {
707
+ return "number";
708
+ }
709
+ if (type === Object || type === "object") {
710
+ return "object";
711
+ }
712
+ if (type === String || type === "string") {
713
+ return "string";
714
+ }
715
+ throw new BadRequestError(`Unknown type: ${String(type)}`);
716
+ }
717
+
718
+ /**
719
+ * Fallback Resolution Helper
720
+ *
721
+ * Resolves a field value with a fallback chain.
722
+ */
723
+ // =============================================================================
724
+ // Helper Functions
725
+ // =============================================================================
726
+ /**
727
+ * Resolve a field value with fallbacks
728
+ *
729
+ * @param entity - The entity to read from
730
+ * @param field - The primary field name
731
+ * @param fallbacks - Fallback field names in priority order
732
+ * @returns First defined value, or undefined
733
+ *
734
+ * @example
735
+ * ```typescript
736
+ * const model = { title: undefined, name: "My Model", description: "A description" };
737
+ * resolveWithFallback(model, "title", ["name", "description"]);
738
+ * // Returns "My Model"
739
+ * ```
740
+ */
741
+ function resolveWithFallback(entity, field, fallbacks = []) {
742
+ // Check primary field
743
+ if (entity[field] !== undefined) {
744
+ return entity[field];
745
+ }
746
+ // Check fallbacks in order
747
+ for (const fallback of fallbacks) {
748
+ if (entity[fallback] !== undefined) {
749
+ return entity[fallback];
750
+ }
751
+ }
752
+ return undefined;
753
+ }
754
+
755
+ /**
756
+ * Resolved Name Helper
757
+ *
758
+ * Computes a resolved name from model fields using a priority chain.
759
+ */
760
+ // =============================================================================
761
+ // Helper Functions
762
+ // =============================================================================
763
+ /**
764
+ * Compute resolved name from model fields
765
+ *
766
+ * Priority: name > alias > abbreviation > description
767
+ * Result is lowercase for consistent sorting/indexing
768
+ *
769
+ * @param model - The model to compute the resolved name from
770
+ * @returns The resolved name (lowercase) or undefined if no name fields exist
771
+ */
772
+ function computeResolvedName(model) {
773
+ const value = model.name || model.alias || model.abbreviation || model.description;
774
+ return value?.toLowerCase();
775
+ }
776
+
777
+ /**
778
+ * Index Types for @jaypie/fabric
779
+ *
780
+ * Declarative index definitions for DynamoDB single-table design.
781
+ * Models can specify their own indexes, and dynamodb will create
782
+ * GSIs and auto-detect which index to use for queries.
783
+ */
784
+ // =============================================================================
785
+ // Default Indexes
786
+ // =============================================================================
787
+ /**
788
+ * Default indexes for the DynamoDB GSI pattern.
789
+ * These are used when a model does not specify custom indexes.
790
+ */
791
+ const DEFAULT_INDEXES = [
792
+ { name: "indexScope", pk: ["scope", "model"], sk: ["sequence"] },
793
+ {
794
+ name: "indexAlias",
795
+ pk: ["scope", "model", "alias"],
796
+ sk: ["sequence"],
797
+ sparse: true,
798
+ },
799
+ {
800
+ name: "indexClass",
801
+ pk: ["scope", "model", "class"],
802
+ sk: ["sequence"],
803
+ sparse: true,
804
+ },
805
+ {
806
+ name: "indexType",
807
+ pk: ["scope", "model", "type"],
808
+ sk: ["sequence"],
809
+ sparse: true,
810
+ },
811
+ {
812
+ name: "indexXid",
813
+ pk: ["scope", "model", "xid"],
814
+ sk: ["sequence"],
815
+ sparse: true,
816
+ },
817
+ ];
818
+ // =============================================================================
819
+ // Constants
820
+ // =============================================================================
821
+ /**
822
+ * Default sort key fields when sk is not specified
823
+ */
824
+ const DEFAULT_SORT_KEY = ["sequence"];
825
+ /**
826
+ * Suffix appended to index keys when model is archived
827
+ */
828
+ const ARCHIVED_SUFFIX = "#archived";
829
+ /**
830
+ * Suffix appended to index keys when model is deleted
831
+ */
832
+ const DELETED_SUFFIX = "#deleted";
833
+
834
+ /**
835
+ * Key Builder for @jaypie/fabric
836
+ *
837
+ * Builds composite keys from model fields and index definitions.
838
+ */
839
+ // =============================================================================
840
+ // Key Builders
841
+ // =============================================================================
842
+ /**
843
+ * Build a composite key from model fields
844
+ *
845
+ * @param model - Model with fields to extract
846
+ * @param fields - Field names to combine
847
+ * @param suffix - Optional suffix to append (e.g., "#deleted")
848
+ * @returns Composite key string (e.g., "@#record#my-alias")
849
+ * @throws ConfigurationError if a required field is missing
850
+ */
851
+ function buildCompositeKey(model, fields, suffix) {
852
+ const parts = fields.map((field) => {
853
+ const value = model[field];
854
+ if (value === undefined || value === null) {
855
+ throw new ConfigurationError(`Missing field for index key: ${field}`);
856
+ }
857
+ return String(value);
858
+ });
859
+ const key = parts.join(SEPARATOR);
860
+ return suffix ? key + suffix : key;
861
+ }
862
+ /**
863
+ * Try to build a composite key, returning undefined if fields are missing
864
+ *
865
+ * @param model - Model with fields to extract
866
+ * @param fields - Field names to combine
867
+ * @param suffix - Optional suffix to append
868
+ * @returns Composite key string or undefined if fields missing
869
+ */
870
+ function tryBuildCompositeKey(model, fields, suffix) {
871
+ for (const field of fields) {
872
+ const value = model[field];
873
+ if (value === undefined || value === null) {
874
+ return undefined;
875
+ }
876
+ }
877
+ return buildCompositeKey(model, fields, suffix);
878
+ }
879
+ /**
880
+ * Generate an index name from partition key fields
881
+ *
882
+ * @param pk - Partition key field names
883
+ * @returns Generated index name (e.g., "indexOuModelAlias")
884
+ */
885
+ function generateIndexName(pk) {
886
+ const suffix = pk
887
+ .map((field) => {
888
+ const str = String(field);
889
+ return str.charAt(0).toUpperCase() + str.slice(1);
890
+ })
891
+ .join("");
892
+ return `index${suffix}`;
893
+ }
894
+ /**
895
+ * Calculate the suffix for index keys based on model state
896
+ *
897
+ * @param model - Model to check for archived/deleted state
898
+ * @returns Suffix string (e.g., "", "#archived", "#deleted", "#archived#deleted")
899
+ */
900
+ function calculateIndexSuffix(model) {
901
+ let suffix = "";
902
+ if (model.archivedAt !== undefined && model.archivedAt !== null) {
903
+ suffix += ARCHIVED_SUFFIX;
904
+ }
905
+ if (model.deletedAt !== undefined && model.deletedAt !== null) {
906
+ suffix += DELETED_SUFFIX;
907
+ }
908
+ return suffix;
909
+ }
910
+ /**
911
+ * Populate index keys on a model based on index definitions
912
+ *
913
+ * Only the partition key composite is stored on the model (e.g., indexOu).
914
+ * The sort key (e.g., sequence) is a regular field that the GSI references directly.
915
+ *
916
+ * @param model - Model to populate index keys on
917
+ * @param indexes - Index definitions to use
918
+ * @param suffix - Optional suffix to append to all index keys
919
+ * @returns Model with index keys populated
920
+ */
921
+ function populateIndexKeys(model, indexes, suffix) {
922
+ const result = { ...model };
923
+ const appliedSuffix = suffix ?? calculateIndexSuffix(model);
924
+ for (const index of indexes) {
925
+ const indexName = index.name ?? generateIndexName(index.pk);
926
+ const pkKey = indexName;
927
+ // For sparse indexes, only populate if all pk fields are present
928
+ if (index.sparse) {
929
+ const pkValue = tryBuildCompositeKey(model, index.pk, appliedSuffix);
930
+ if (pkValue !== undefined) {
931
+ result[pkKey] = pkValue;
932
+ }
933
+ }
934
+ else {
935
+ // For non-sparse indexes, always try to populate (will throw if fields missing)
936
+ const pkValue = tryBuildCompositeKey(model, index.pk, appliedSuffix);
937
+ if (pkValue !== undefined) {
938
+ result[pkKey] = pkValue;
939
+ }
940
+ }
941
+ }
942
+ return result;
943
+ }
944
+ /**
945
+ * Calculate scope from parent reference
946
+ *
947
+ * @param parent - Parent model with model and id
948
+ * @returns Scope string ("{parent.model}#{parent.id}") or APEX ("@") if no parent
949
+ */
950
+ function calculateScope(parent) {
951
+ if (!parent) {
952
+ return "@"; // APEX
953
+ }
954
+ return `${parent.model}${SEPARATOR}${parent.id}`;
955
+ }
956
+
957
+ /**
958
+ * Model Registry for @jaypie/fabric
959
+ *
960
+ * Stores model schemas with their index definitions.
961
+ * DynamoDB reads from this registry to create GSIs and select indexes for queries.
962
+ */
963
+ // =============================================================================
964
+ // Registry
965
+ // =============================================================================
966
+ /**
967
+ * Global model registry - maps model names to their schemas
968
+ */
969
+ const MODEL_REGISTRY = new Map();
970
+ // =============================================================================
971
+ // Functions
972
+ // =============================================================================
973
+ /**
974
+ * Register a model schema with its index definitions
975
+ *
976
+ * @param schema - Model schema with model name and optional indexes
977
+ */
978
+ function registerModel(schema) {
979
+ MODEL_REGISTRY.set(schema.model, schema);
980
+ }
981
+ /**
982
+ * Get a model schema by name
983
+ *
984
+ * @param model - Model name to look up
985
+ * @returns Model schema or undefined if not registered
986
+ */
987
+ function getModelSchema(model) {
988
+ return MODEL_REGISTRY.get(model);
989
+ }
990
+ /**
991
+ * Get index definitions for a model
992
+ *
993
+ * Returns the model's custom indexes if registered,
994
+ * otherwise returns DEFAULT_INDEXES.
995
+ *
996
+ * @param model - Model name to get indexes for
997
+ * @returns Array of index definitions
998
+ */
999
+ function getModelIndexes(model) {
1000
+ const schema = MODEL_REGISTRY.get(model);
1001
+ return schema?.indexes ?? DEFAULT_INDEXES;
1002
+ }
1003
+ /**
1004
+ * Get all registered models
1005
+ *
1006
+ * @returns Array of model names
1007
+ */
1008
+ function getRegisteredModels() {
1009
+ return Array.from(MODEL_REGISTRY.keys());
1010
+ }
1011
+ /**
1012
+ * Get all unique indexes across all registered models
1013
+ *
1014
+ * Used by createTable to collect all GSIs that need to be created.
1015
+ * Deduplicates by index name.
1016
+ *
1017
+ * @returns Array of unique index definitions
1018
+ */
1019
+ function getAllRegisteredIndexes() {
1020
+ const indexMap = new Map();
1021
+ // Collect indexes from all registered models
1022
+ for (const schema of MODEL_REGISTRY.values()) {
1023
+ const indexes = schema.indexes ?? [];
1024
+ for (const index of indexes) {
1025
+ const name = index.name ?? generateIndexNameFromPk(index.pk);
1026
+ if (!indexMap.has(name)) {
1027
+ indexMap.set(name, { ...index, name });
1028
+ }
1029
+ }
1030
+ }
1031
+ return Array.from(indexMap.values());
1032
+ }
1033
+ /**
1034
+ * Check if a model is registered
1035
+ *
1036
+ * @param model - Model name to check
1037
+ * @returns true if model is registered
1038
+ */
1039
+ function isModelRegistered(model) {
1040
+ return MODEL_REGISTRY.has(model);
1041
+ }
1042
+ /**
1043
+ * Clear the model registry (for testing)
1044
+ */
1045
+ function clearRegistry() {
1046
+ MODEL_REGISTRY.clear();
1047
+ }
1048
+ // =============================================================================
1049
+ // Helpers
1050
+ // =============================================================================
1051
+ /**
1052
+ * Generate an index name from pk fields (used when index.name is not set)
1053
+ */
1054
+ function generateIndexNameFromPk(pk) {
1055
+ const suffix = pk
1056
+ .map((field) => {
1057
+ const str = String(field);
1058
+ return str.charAt(0).toUpperCase() + str.slice(1);
1059
+ })
1060
+ .join("");
1061
+ return `index${suffix}`;
1062
+ }
1063
+
1064
+ /**
1065
+ * Elementary Field Types
1066
+ *
1067
+ * Defines the 10 elementary field types for the Fabric vocabulary.
1068
+ * Each type has: alias, name, icon, and optional validation/format rules.
1069
+ */
1070
+ // =============================================================================
1071
+ // Constants
1072
+ // =============================================================================
1073
+ /** Elementary type aliases */
1074
+ const ELEMENTARY_TYPES = [
1075
+ "boolean",
1076
+ "date",
1077
+ "datetime",
1078
+ "dollars",
1079
+ "multiselect",
1080
+ "number",
1081
+ "reference",
1082
+ "select",
1083
+ "text",
1084
+ "textarea",
1085
+ ];
1086
+ // =============================================================================
1087
+ // Type Guards
1088
+ // =============================================================================
1089
+ /**
1090
+ * Check if a string is an elementary type
1091
+ */
1092
+ function isElementaryType(value) {
1093
+ return (typeof value === "string" &&
1094
+ ELEMENTARY_TYPES.includes(value));
1095
+ }
1096
+ // =============================================================================
1097
+ // Elementary Type Definitions
1098
+ // =============================================================================
1099
+ /**
1100
+ * Boolean - True/false toggle
1101
+ */
1102
+ const BOOLEAN_TYPE = {
1103
+ alias: "boolean",
1104
+ defaultValue: false,
1105
+ description: "True/false toggle",
1106
+ icon: "lucide#toggle-left",
1107
+ inputComponent: "checkbox",
1108
+ name: "Boolean",
1109
+ type: "boolean",
1110
+ };
1111
+ /**
1112
+ * Date - Date picker (ISO format)
1113
+ */
1114
+ const DATE_TYPE = {
1115
+ alias: "date",
1116
+ description: "Date value (YYYY-MM-DD)",
1117
+ formatPattern: "YYYY-MM-DD",
1118
+ icon: "lucide#calendar",
1119
+ inputComponent: "date-picker",
1120
+ name: "Date",
1121
+ type: "date",
1122
+ };
1123
+ /**
1124
+ * Datetime - Date + time picker
1125
+ */
1126
+ const DATETIME_TYPE = {
1127
+ alias: "datetime",
1128
+ description: "Date and time value (ISO 8601)",
1129
+ formatPattern: "YYYY-MM-DDTHH:mm:ssZ",
1130
+ icon: "lucide#calendar-clock",
1131
+ inputComponent: "datetime-picker",
1132
+ name: "Date & Time",
1133
+ type: "datetime",
1134
+ };
1135
+ /**
1136
+ * Dollars - Currency with formatting
1137
+ */
1138
+ const DOLLARS_TYPE = {
1139
+ alias: "dollars",
1140
+ description: "Currency value formatted as $X.XX",
1141
+ formatPattern: "$0,0.00",
1142
+ icon: "lucide#dollar-sign",
1143
+ inputComponent: "currency-input",
1144
+ name: "Dollars",
1145
+ type: "dollars",
1146
+ validation: [
1147
+ { message: "Must be a valid number", type: "numeric" },
1148
+ { message: "Cannot be negative", type: "min", value: 0 },
1149
+ ],
1150
+ };
1151
+ /**
1152
+ * Multiselect - Multiple selections from options
1153
+ */
1154
+ const MULTISELECT_TYPE = {
1155
+ alias: "multiselect",
1156
+ description: "Multiple selections from predefined options",
1157
+ icon: "lucide#list-checks",
1158
+ inputComponent: "multiselect",
1159
+ name: "Multi-Select",
1160
+ options: [], // Populated per-field
1161
+ type: "multiselect",
1162
+ };
1163
+ /**
1164
+ * Number - Numeric input (integer or float)
1165
+ */
1166
+ const NUMBER_TYPE = {
1167
+ alias: "number",
1168
+ description: "Numeric value (integer or decimal)",
1169
+ icon: "lucide#hash",
1170
+ inputComponent: "input[type=number]",
1171
+ name: "Number",
1172
+ type: "number",
1173
+ validation: [{ message: "Must be a valid number", type: "numeric" }],
1174
+ };
1175
+ /**
1176
+ * Reference - Link to another model
1177
+ */
1178
+ const REFERENCE_TYPE = {
1179
+ alias: "reference",
1180
+ description: "Link to another model by id or alias",
1181
+ icon: "lucide#link",
1182
+ inputComponent: "entity-picker",
1183
+ name: "Reference",
1184
+ referenceModel: undefined, // Set per-field
1185
+ type: "reference",
1186
+ };
1187
+ /**
1188
+ * Select - Single selection from options
1189
+ */
1190
+ const SELECT_TYPE = {
1191
+ alias: "select",
1192
+ description: "Single selection from predefined options",
1193
+ icon: "lucide#list",
1194
+ inputComponent: "select",
1195
+ name: "Select",
1196
+ options: [], // Populated per-field
1197
+ type: "select",
1198
+ };
1199
+ /**
1200
+ * Text - Single-line string input
1201
+ */
1202
+ const TEXT_TYPE = {
1203
+ alias: "text",
1204
+ description: "Single-line text input",
1205
+ icon: "lucide#type",
1206
+ inputComponent: "input",
1207
+ name: "Text",
1208
+ type: "text",
1209
+ };
1210
+ /**
1211
+ * Textarea - Multi-line string input
1212
+ */
1213
+ const TEXTAREA_TYPE = {
1214
+ alias: "textarea",
1215
+ description: "Multi-line text input",
1216
+ icon: "lucide#align-left",
1217
+ inputComponent: "textarea",
1218
+ name: "Text Area",
1219
+ type: "textarea",
1220
+ };
1221
+ // =============================================================================
1222
+ // Type Registry
1223
+ // =============================================================================
1224
+ /**
1225
+ * Registry of all elementary types
1226
+ */
1227
+ const ELEMENTARY_TYPE_REGISTRY = {
1228
+ boolean: BOOLEAN_TYPE,
1229
+ date: DATE_TYPE,
1230
+ datetime: DATETIME_TYPE,
1231
+ dollars: DOLLARS_TYPE,
1232
+ multiselect: MULTISELECT_TYPE,
1233
+ number: NUMBER_TYPE,
1234
+ reference: REFERENCE_TYPE,
1235
+ select: SELECT_TYPE,
1236
+ text: TEXT_TYPE,
1237
+ textarea: TEXTAREA_TYPE,
1238
+ };
1239
+ // =============================================================================
1240
+ // Helper Functions
1241
+ // =============================================================================
1242
+ /**
1243
+ * Get an elementary type definition by alias
1244
+ */
1245
+ function getElementaryType(alias) {
1246
+ return ELEMENTARY_TYPE_REGISTRY[alias];
1247
+ }
1248
+ /**
1249
+ * Get all elementary type definitions
1250
+ */
1251
+ function getAllElementaryTypes() {
1252
+ return Object.values(ELEMENTARY_TYPE_REGISTRY);
1253
+ }
1254
+
1255
+ /**
1256
+ * Field Definition - Describes a field's structure and behavior
1257
+ *
1258
+ * Field definitions are the building blocks of model definitions,
1259
+ * specifying type, validation, and metadata for each field.
1260
+ */
1261
+ // =============================================================================
1262
+ // Type Guards
1263
+ // =============================================================================
1264
+ /**
1265
+ * Check if a field ref is an inline definition
1266
+ */
1267
+ function isFieldDefinition(ref) {
1268
+ return typeof ref === "object" && "alias" in ref && "type" in ref;
1269
+ }
1270
+
1271
+ // Service for @jaypie/fabric
1272
+ /**
1273
+ * Check if a single-element array is a typed array type constructor.
1274
+ */
1275
+ function isTypedArrayConstructor(element) {
1276
+ return (element === Boolean ||
1277
+ element === Number ||
1278
+ element === String ||
1279
+ element === Object ||
1280
+ element === "boolean" ||
1281
+ element === "number" ||
1282
+ element === "string" ||
1283
+ element === "object" ||
1284
+ element === "" ||
1285
+ (typeof element === "object" &&
1286
+ element !== null &&
1287
+ !(element instanceof RegExp) &&
1288
+ Object.keys(element).length === 0));
1289
+ }
1290
+ /**
1291
+ * Check if a type is a validated string type (array of string literals and/or RegExp).
1292
+ * Distinguishes from typed arrays like [String], [Number], etc.
1293
+ */
1294
+ function isValidatedStringType(type) {
1295
+ if (!Array.isArray(type)) {
1296
+ return false;
1297
+ }
1298
+ // Empty array is untyped array, not validated string
1299
+ if (type.length === 0) {
1300
+ return false;
1301
+ }
1302
+ // Single-element arrays with type constructors are typed arrays
1303
+ if (type.length === 1 && isTypedArrayConstructor(type[0])) {
1304
+ return false;
1305
+ }
1306
+ // Check that all elements are strings or RegExp
1307
+ return type.every((item) => typeof item === "string" || item instanceof RegExp);
1308
+ }
1309
+ /**
1310
+ * Check if a type is a validated number type (array of number literals).
1311
+ * Distinguishes from typed arrays like [Number], etc.
1312
+ */
1313
+ function isValidatedNumberType(type) {
1314
+ if (!Array.isArray(type)) {
1315
+ return false;
1316
+ }
1317
+ // Empty array is untyped array, not validated number
1318
+ if (type.length === 0) {
1319
+ return false;
1320
+ }
1321
+ // Single-element arrays with type constructors are typed arrays
1322
+ if (type.length === 1 && isTypedArrayConstructor(type[0])) {
1323
+ return false;
1324
+ }
1325
+ // Check that all elements are numbers
1326
+ return type.every((item) => typeof item === "number");
1327
+ }
1328
+ /**
1329
+ * Parse input string as JSON if it's a string
1330
+ */
1331
+ function parseInput(input) {
1332
+ if (input === undefined || input === null) {
1333
+ return {};
1334
+ }
1335
+ if (typeof input === "string") {
1336
+ if (input === "") {
1337
+ return {};
1338
+ }
1339
+ try {
1340
+ const parsed = JSON.parse(input);
1341
+ if (typeof parsed !== "object" ||
1342
+ parsed === null ||
1343
+ Array.isArray(parsed)) {
1344
+ throw new BadRequestError("Input must be an object");
1345
+ }
1346
+ return parsed;
1347
+ }
1348
+ catch (error) {
1349
+ if (error instanceof BadRequestError) {
1350
+ throw error;
1351
+ }
1352
+ throw new BadRequestError("Invalid JSON input");
1353
+ }
1354
+ }
1355
+ if (typeof input === "object" && !Array.isArray(input)) {
1356
+ return input;
1357
+ }
1358
+ throw new BadRequestError("Input must be an object or JSON string");
1359
+ }
1360
+ /**
1361
+ * Run validation on a value (supports async validators)
1362
+ */
1363
+ async function runValidation(value, validate, fieldName) {
1364
+ if (typeof validate === "function") {
1365
+ const result = await validate(value);
1366
+ if (result === false) {
1367
+ throw new BadRequestError(`Validation failed for field "${fieldName}"`);
1368
+ }
1369
+ }
1370
+ else if (validate instanceof RegExp) {
1371
+ if (typeof value !== "string" || !validate.test(value)) {
1372
+ throw new BadRequestError(`Validation failed for field "${fieldName}"`);
1373
+ }
1374
+ }
1375
+ else if (Array.isArray(validate)) {
1376
+ // Check if value matches any item in the array
1377
+ for (const item of validate) {
1378
+ if (item instanceof RegExp) {
1379
+ if (typeof value === "string" && item.test(value)) {
1380
+ return; // Match found
1381
+ }
1382
+ }
1383
+ else if (typeof item === "function") {
1384
+ try {
1385
+ const result = await item(value);
1386
+ if (result !== false) {
1387
+ return; // Match found
1388
+ }
1389
+ }
1390
+ catch {
1391
+ // Continue to next item
1392
+ }
1393
+ }
1394
+ else if (value === item) {
1395
+ return; // Scalar match found
1396
+ }
1397
+ }
1398
+ throw new BadRequestError(`Validation failed for field "${fieldName}"`);
1399
+ }
1400
+ }
1401
+ /**
1402
+ * Check if a field is required
1403
+ * A field is required unless it has a default OR required is explicitly false
1404
+ */
1405
+ function isFieldRequired(definition) {
1406
+ if (definition.required === false) {
1407
+ return false;
1408
+ }
1409
+ if (definition.default !== undefined) {
1410
+ return false;
1411
+ }
1412
+ return true;
1413
+ }
1414
+ /**
1415
+ * Process a single field through conversion and validation
1416
+ */
1417
+ async function processField(fieldName, value, definition) {
1418
+ // Apply default if value is undefined
1419
+ let processedValue = value;
1420
+ if (processedValue === undefined && definition.default !== undefined) {
1421
+ processedValue = definition.default;
1422
+ }
1423
+ // Determine actual type and validation
1424
+ let actualType = definition.type;
1425
+ let validation = definition.validate;
1426
+ // Handle bare RegExp shorthand: /regex/
1427
+ if (definition.type instanceof RegExp) {
1428
+ actualType = String;
1429
+ validation = definition.type; // The RegExp becomes the validation
1430
+ }
1431
+ // Handle validated string shorthand: ["value1", "value2"] or [/regex/]
1432
+ else if (isValidatedStringType(definition.type)) {
1433
+ actualType = String;
1434
+ validation = definition.type; // The array becomes the validation
1435
+ }
1436
+ // Handle validated number shorthand: [1, 2, 3]
1437
+ else if (isValidatedNumberType(definition.type)) {
1438
+ actualType = Number;
1439
+ validation = definition.type; // The array becomes the validation
1440
+ }
1441
+ // Fabric to target type
1442
+ const convertedValue = fabric(processedValue, actualType);
1443
+ // Check if required field is missing
1444
+ if (convertedValue === undefined && isFieldRequired(definition)) {
1445
+ throw new BadRequestError(`Missing required field "${fieldName}"`);
1446
+ }
1447
+ // Run validation if provided
1448
+ if (validation !== undefined && convertedValue !== undefined) {
1449
+ await runValidation(convertedValue, validation, fieldName);
1450
+ }
1451
+ return convertedValue;
1452
+ }
1453
+ /**
1454
+ * Fabric a service function
1455
+ *
1456
+ * Service builds a function that initiates a "controller" step that:
1457
+ * - Parses the input if it is a string to object
1458
+ * - Fabrics each input field to its type
1459
+ * - Calls the validation function or regular expression or checks the array
1460
+ * - Calls the service function and returns the response
1461
+ *
1462
+ * The returned function has config properties for introspection.
1463
+ */
1464
+ function fabricService(config) {
1465
+ const { input: inputDefinitions, service } = config;
1466
+ const handler = async (rawInput, context) => {
1467
+ // Parse input (handles string JSON)
1468
+ const parsedInput = parseInput(rawInput);
1469
+ // If no input definitions, pass through to service or return parsed input
1470
+ if (!inputDefinitions) {
1471
+ if (service) {
1472
+ return service(parsedInput, context);
1473
+ }
1474
+ return parsedInput;
1475
+ }
1476
+ // Process all fields in parallel
1477
+ const entries = Object.entries(inputDefinitions);
1478
+ const processedValues = await Promise.all(entries.map(([fieldName, definition]) => processField(fieldName, parsedInput[fieldName], definition)));
1479
+ // Build processed input object
1480
+ const processedInput = {};
1481
+ entries.forEach(([fieldName], index) => {
1482
+ processedInput[fieldName] = processedValues[index];
1483
+ });
1484
+ // Return processed input if no service, otherwise call service
1485
+ if (service) {
1486
+ return service(processedInput, context);
1487
+ }
1488
+ return processedInput;
1489
+ };
1490
+ // Attach config properties directly to handler for flat access
1491
+ const typedHandler = handler;
1492
+ typedHandler.$fabric = FABRIC_VERSION;
1493
+ if (config.alias !== undefined)
1494
+ typedHandler.alias = config.alias;
1495
+ if (config.description !== undefined)
1496
+ typedHandler.description = config.description;
1497
+ if (config.input !== undefined)
1498
+ typedHandler.input = config.input;
1499
+ if (config.service !== undefined)
1500
+ typedHandler.service = config.service;
1501
+ return typedHandler;
1502
+ }
1503
+
1504
+ // Resolve inline service definitions to full Service objects
1505
+ /**
1506
+ * Type guard to check if a value is a pre-instantiated Service
1507
+ * A Service is a function with the `$fabric` property set by fabricService
1508
+ */
1509
+ function isService(value) {
1510
+ return typeof value === "function" && "$fabric" in value;
1511
+ }
1512
+ /**
1513
+ * Resolve a service configuration to a full Service object
1514
+ *
1515
+ * Supports two patterns:
1516
+ * 1. Inline service definition - pass a plain function as `service` along with
1517
+ * `alias`, `description`, and `input` in the config
1518
+ * 2. Pre-instantiated Service - pass a Service object as `service`
1519
+ *
1520
+ * When a pre-instantiated Service is passed, config fields act as overrides:
1521
+ * - `alias` overrides service.alias
1522
+ * - `description` overrides service.description
1523
+ * - `input` overrides service.input
1524
+ *
1525
+ * The original Service is never mutated - a new Service is created when overrides
1526
+ * are applied.
1527
+ *
1528
+ * @example
1529
+ * ```typescript
1530
+ * // Inline service definition
1531
+ * const service = resolveService({
1532
+ * alias: "greet",
1533
+ * description: "Greet a user",
1534
+ * input: { name: { type: String } },
1535
+ * service: ({ name }) => `Hello, ${name}!`,
1536
+ * });
1537
+ *
1538
+ * // Pre-instantiated with override
1539
+ * const baseService = fabricService({ alias: "foo", service: (x) => x });
1540
+ * const overridden = resolveService({
1541
+ * alias: "bar", // Override alias
1542
+ * service: baseService,
1543
+ * });
1544
+ * ```
1545
+ */
1546
+ function resolveService(config) {
1547
+ const { alias, description, input, service } = config;
1548
+ if (isService(service)) {
1549
+ // Service is pre-instantiated - config fields act as overrides
1550
+ // Create new Service with merged properties (config overrides service)
1551
+ return fabricService({
1552
+ alias: alias ?? service.alias,
1553
+ description: description ?? service.description,
1554
+ input: input ?? service.input,
1555
+ service: service.service,
1556
+ });
1557
+ }
1558
+ // Service is an inline function - create Service from config
1559
+ return fabricService({
1560
+ alias,
1561
+ description,
1562
+ input,
1563
+ service,
1564
+ });
1565
+ }
1566
+
1567
+ /**
1568
+ * Status Type for @jaypie/fabric
1569
+ *
1570
+ * Standard status values for fields that track processing state.
1571
+ */
1572
+ /**
1573
+ * Valid status values for status fields
1574
+ */
1575
+ const STATUS_VALUES = [
1576
+ "canceled",
1577
+ "complete",
1578
+ "error",
1579
+ "pending",
1580
+ "processing",
1581
+ "queued",
1582
+ "sending",
1583
+ ];
1584
+ /**
1585
+ * Status type for use in createService input definitions
1586
+ *
1587
+ * Usage:
1588
+ * ```typescript
1589
+ * const handler = createService({
1590
+ * input: {
1591
+ * status: { type: StatusType, description: "Current status" },
1592
+ * },
1593
+ * });
1594
+ * ```
1595
+ */
1596
+ const StatusType = [...STATUS_VALUES];
1597
+ /**
1598
+ * Check if a value is a valid status
1599
+ */
1600
+ function isStatus(value) {
1601
+ return (typeof value === "string" &&
1602
+ STATUS_VALUES.includes(value));
1603
+ }
1604
+
1605
+ export { APEX, ARCHIVED_SUFFIX, BOOLEAN_TYPE, DATETIME_TYPE, DATE_TYPE, DEFAULT_INDEXES, DEFAULT_SORT_KEY, DELETED_SUFFIX, DOLLARS_TYPE, DateType, ELEMENTARY_TYPES, ELEMENTARY_TYPE_REGISTRY, FABRIC_MODEL_AUTO_FIELDS, FABRIC_MODEL_FIELDS, FABRIC_MODEL_REQUIRED_FIELDS, FABRIC_MODEL_TIMESTAMP_FIELDS, FABRIC_VERSION, MULTISELECT_TYPE, NUMBER_TYPE, REFERENCE_TYPE, SELECT_TYPE, SEPARATOR, STATUS_VALUES, SYSTEM_MODELS, StatusType, TEXTAREA_TYPE, TEXT_TYPE, buildCompositeKey, calculateIndexSuffix, calculateScope, clearRegistry, computeResolvedName, createFabricModelInput, fabric, fabricArray, fabricBoolean, fabricDate, fabricNumber, fabricObject, fabricService, fabricString, generateIndexName, getAllElementaryTypes, getAllRegisteredIndexes, getElementaryType, getModelIndexes, getModelSchema, getRegisteredModels, hasFabricModelShape, isAutoField, isDateType, isElementaryType, isFabricModel, isFieldDefinition, isModelRegistered, isStatus, isTimestampField, isValidDate, pickFabricModelFields, populateIndexKeys, registerModel, resolveFromArray, resolveFromDate, resolveFromObject, resolveService, resolveWithFallback, tryBuildCompositeKey };
1606
+ //# sourceMappingURL=index.js.map