@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,1548 @@
1
+ import { BadRequestError, UnauthorizedError, NotFoundError } from '@jaypie/errors';
2
+
3
+ // #region Constants
4
+ /**
5
+ * Default pagination limit for list operations
6
+ */
7
+ const DEFAULT_LIMIT = 20;
8
+ /**
9
+ * Maximum pagination limit
10
+ */
11
+ const MAX_LIMIT = 100;
12
+ // #endregion
13
+
14
+ /**
15
+ * Default scope value (APEX)
16
+ */
17
+ const APEX = "@";
18
+ /**
19
+ * Extract ID from path parameters
20
+ */
21
+ function extractId(context) {
22
+ return context.params.id;
23
+ }
24
+ /**
25
+ * Extract scope context from HTTP context
26
+ */
27
+ function extractScopeContext(context) {
28
+ return {
29
+ body: context.body,
30
+ params: context.params,
31
+ query: context.query,
32
+ };
33
+ }
34
+ /**
35
+ * Calculate scope from scope configuration
36
+ */
37
+ async function calculateScopeFromConfig(scopeConfig, context) {
38
+ if (scopeConfig === undefined) {
39
+ return APEX;
40
+ }
41
+ if (typeof scopeConfig === "string") {
42
+ return scopeConfig;
43
+ }
44
+ const scopeContext = extractScopeContext(context);
45
+ return scopeConfig(scopeContext);
46
+ }
47
+ /**
48
+ * Transform HTTP context to create operation input
49
+ * Extracts body fields for entity creation
50
+ */
51
+ function transformCreate(context) {
52
+ const body = context.body;
53
+ return { ...body };
54
+ }
55
+ /**
56
+ * Transform HTTP context to read operation input
57
+ * Extracts ID from path parameters
58
+ */
59
+ function transformRead(context) {
60
+ const id = extractId(context);
61
+ if (!id) {
62
+ throw new Error("Missing id parameter");
63
+ }
64
+ return { id };
65
+ }
66
+ /**
67
+ * Transform HTTP context to update operation input
68
+ * Extracts ID from path and merges with body
69
+ */
70
+ function transformUpdate(context) {
71
+ const id = extractId(context);
72
+ if (!id) {
73
+ throw new Error("Missing id parameter");
74
+ }
75
+ const body = context.body;
76
+ return { id, ...body };
77
+ }
78
+ /**
79
+ * Transform HTTP context to delete operation input
80
+ * Extracts ID from path parameters
81
+ */
82
+ function transformDelete(context) {
83
+ const id = extractId(context);
84
+ if (!id) {
85
+ throw new Error("Missing id parameter");
86
+ }
87
+ return { id };
88
+ }
89
+ /**
90
+ * Transform HTTP context to archive operation input
91
+ * Extracts ID from path parameters
92
+ */
93
+ function transformArchive(context) {
94
+ const id = extractId(context);
95
+ if (!id) {
96
+ throw new Error("Missing id parameter");
97
+ }
98
+ return { id };
99
+ }
100
+ /**
101
+ * Transform HTTP context to list operation input
102
+ * Extracts pagination options from query string
103
+ */
104
+ function transformList(context, defaultLimit = DEFAULT_LIMIT, maxLimit = MAX_LIMIT) {
105
+ const query = context.query;
106
+ // Parse limit with bounds
107
+ let limit = defaultLimit;
108
+ const limitParam = query.get("limit");
109
+ if (limitParam) {
110
+ const parsed = parseInt(limitParam, 10);
111
+ if (!isNaN(parsed) && parsed > 0) {
112
+ limit = Math.min(parsed, maxLimit);
113
+ }
114
+ }
115
+ // Parse cursor
116
+ const startKey = query.get("cursor") ?? query.get("startKey") ?? undefined;
117
+ // Parse sort order
118
+ const ascending = query.get("ascending") === "true" || query.get("sort") === "asc";
119
+ // Parse archived/deleted flags
120
+ const archived = query.get("archived") === "true";
121
+ const deleted = query.get("deleted") === "true";
122
+ return {
123
+ archived,
124
+ ascending,
125
+ deleted,
126
+ limit,
127
+ startKey,
128
+ };
129
+ }
130
+ /**
131
+ * Transform HTTP context to execute operation input
132
+ * Extracts ID from path and merges with body
133
+ */
134
+ function transformExecute(context) {
135
+ const id = extractId(context);
136
+ if (!id) {
137
+ throw new Error("Missing id parameter");
138
+ }
139
+ const body = context.body;
140
+ return { id, ...body };
141
+ }
142
+ /**
143
+ * Pluralize a model alias for route paths
144
+ * Simple pluralization: adds 's' unless already ends in 's'
145
+ */
146
+ function pluralize(alias) {
147
+ if (alias.endsWith("s")) {
148
+ return alias;
149
+ }
150
+ // Handle common irregular plurals
151
+ if (alias.endsWith("y")) {
152
+ return alias.slice(0, -1) + "ies";
153
+ }
154
+ if (alias.endsWith("x") || alias.endsWith("ch") || alias.endsWith("sh")) {
155
+ return alias + "es";
156
+ }
157
+ return alias + "s";
158
+ }
159
+ /**
160
+ * Capitalize first letter of string
161
+ */
162
+ function capitalize(str) {
163
+ if (!str)
164
+ return str;
165
+ return str.charAt(0).toUpperCase() + str.slice(1);
166
+ }
167
+ /**
168
+ * Encode pagination cursor for client response
169
+ */
170
+ function encodeCursor(lastEvaluatedKey) {
171
+ if (!lastEvaluatedKey) {
172
+ return undefined;
173
+ }
174
+ return Buffer.from(JSON.stringify(lastEvaluatedKey)).toString("base64");
175
+ }
176
+ /**
177
+ * Decode pagination cursor from client request
178
+ */
179
+ function decodeCursor(cursor) {
180
+ if (!cursor) {
181
+ return undefined;
182
+ }
183
+ try {
184
+ const decoded = Buffer.from(cursor, "base64").toString("utf-8");
185
+ return JSON.parse(decoded);
186
+ }
187
+ catch {
188
+ return undefined;
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Meta-modeling Constants
194
+ */
195
+ // =============================================================================
196
+ // Constants
197
+ // =============================================================================
198
+ /** Root organizational unit */
199
+ /** Fabric version - used to identify pre-instantiated Services */
200
+ const FABRIC_VERSION = "0.1.0";
201
+
202
+ /**
203
+ * Date Type Conversion for @jaypie/fabric
204
+ *
205
+ * Adds Date as a supported type in the fabric type system.
206
+ * Follows the same conversion patterns as String, Number, Boolean.
207
+ */
208
+ /**
209
+ * Convert a value to a Date
210
+ *
211
+ * Supported inputs:
212
+ * - Date: returned as-is (validated)
213
+ * - Number: treated as Unix timestamp (milliseconds)
214
+ * - String: parsed via Date constructor (ISO 8601, etc.)
215
+ * - Object with value property: unwrapped and converted
216
+ *
217
+ * @throws BadRequestError if value cannot be converted to valid Date
218
+ */
219
+ function fabricDate(value) {
220
+ // Already a Date
221
+ if (value instanceof Date) {
222
+ if (Number.isNaN(value.getTime())) {
223
+ throw new BadRequestError("Invalid Date value");
224
+ }
225
+ return value;
226
+ }
227
+ // Null/undefined
228
+ if (value === null || value === undefined) {
229
+ throw new BadRequestError("Cannot convert null or undefined to Date");
230
+ }
231
+ // Object with value property (fabric pattern)
232
+ if (typeof value === "object" && value !== null && "value" in value) {
233
+ return fabricDate(value.value);
234
+ }
235
+ // Number (timestamp in milliseconds)
236
+ if (typeof value === "number") {
237
+ if (Number.isNaN(value)) {
238
+ throw new BadRequestError("Cannot convert NaN to Date");
239
+ }
240
+ const date = new Date(value);
241
+ if (Number.isNaN(date.getTime())) {
242
+ throw new BadRequestError(`Cannot convert ${value} to Date`);
243
+ }
244
+ return date;
245
+ }
246
+ // String (ISO 8601 or parseable format)
247
+ if (typeof value === "string") {
248
+ // Empty string is invalid
249
+ if (value.trim() === "") {
250
+ throw new BadRequestError("Cannot convert empty string to Date");
251
+ }
252
+ const date = new Date(value);
253
+ if (Number.isNaN(date.getTime())) {
254
+ throw new BadRequestError(`Cannot convert "${value}" to Date`);
255
+ }
256
+ return date;
257
+ }
258
+ // Boolean cannot be converted to Date
259
+ if (typeof value === "boolean") {
260
+ throw new BadRequestError("Cannot convert boolean to Date");
261
+ }
262
+ // Arrays - attempt single element extraction
263
+ if (Array.isArray(value)) {
264
+ if (value.length === 1) {
265
+ return fabricDate(value[0]);
266
+ }
267
+ throw new BadRequestError(`Cannot convert array with ${value.length} elements to Date`);
268
+ }
269
+ throw new BadRequestError(`Cannot convert ${typeof value} to Date`);
270
+ }
271
+ /**
272
+ * Type guard for Date type in schema definitions
273
+ */
274
+ function isDateType(type) {
275
+ return type === Date;
276
+ }
277
+
278
+ // Fabric functions for @jaypie/fabric
279
+ /**
280
+ * Try to parse a string as JSON if it looks like JSON
281
+ * Returns the parsed value or the original string if not JSON
282
+ */
283
+ function tryParseJson(value) {
284
+ const trimmed = value.trim();
285
+ if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
286
+ (trimmed.startsWith("[") && trimmed.endsWith("]"))) {
287
+ try {
288
+ return JSON.parse(trimmed);
289
+ }
290
+ catch {
291
+ // Not valid JSON, return original
292
+ return value;
293
+ }
294
+ }
295
+ return value;
296
+ }
297
+ /**
298
+ * Unwrap arrays and objects to get to the scalar value
299
+ * - Single-element arrays unwrap to their element
300
+ * - Objects with value property unwrap to that value
301
+ * - Recursively unwraps nested structures
302
+ */
303
+ function unwrapToScalar(value) {
304
+ if (value === undefined || value === null) {
305
+ return value;
306
+ }
307
+ // Unwrap single-element arrays
308
+ if (Array.isArray(value)) {
309
+ if (value.length === 0) {
310
+ return undefined;
311
+ }
312
+ if (value.length === 1) {
313
+ return unwrapToScalar(value[0]);
314
+ }
315
+ throw new BadRequestError("Cannot convert multi-value array to scalar");
316
+ }
317
+ // Unwrap objects with value property
318
+ if (typeof value === "object") {
319
+ const obj = value;
320
+ if ("value" in obj) {
321
+ return unwrapToScalar(obj.value);
322
+ }
323
+ throw new BadRequestError("Object must have a value attribute");
324
+ }
325
+ return value;
326
+ }
327
+ /**
328
+ * Prepare a value for scalar conversion by parsing JSON strings and unwrapping
329
+ */
330
+ function prepareForScalarConversion(value) {
331
+ if (value === undefined || value === null) {
332
+ return value;
333
+ }
334
+ // Try to parse JSON strings
335
+ if (typeof value === "string") {
336
+ const parsed = tryParseJson(value);
337
+ if (parsed !== value) {
338
+ // Successfully parsed, unwrap the result
339
+ return unwrapToScalar(parsed);
340
+ }
341
+ return value;
342
+ }
343
+ // Unwrap arrays and objects
344
+ if (Array.isArray(value) || typeof value === "object") {
345
+ return unwrapToScalar(value);
346
+ }
347
+ return value;
348
+ }
349
+ /**
350
+ * Convert a value to a boolean
351
+ * - Arrays, objects, and JSON strings are unwrapped first
352
+ * - String "true" becomes true
353
+ * - String "false" becomes false
354
+ * - Strings that parse to numbers: positive = true, zero or negative = false
355
+ * - Numbers: positive = true, zero or negative = false
356
+ * - Boolean passes through
357
+ */
358
+ function fabricBoolean(value) {
359
+ // Prepare value by parsing JSON and unwrapping arrays/objects
360
+ const prepared = prepareForScalarConversion(value);
361
+ if (prepared === undefined || prepared === null) {
362
+ return undefined;
363
+ }
364
+ if (typeof prepared === "boolean") {
365
+ return prepared;
366
+ }
367
+ if (typeof prepared === "string") {
368
+ if (prepared === "") {
369
+ return undefined;
370
+ }
371
+ const lower = prepared.toLowerCase();
372
+ if (lower === "true") {
373
+ return true;
374
+ }
375
+ if (lower === "false") {
376
+ return false;
377
+ }
378
+ // Try to parse as number
379
+ const num = parseFloat(prepared);
380
+ if (isNaN(num)) {
381
+ throw new BadRequestError(`Cannot convert "${prepared}" to Boolean`);
382
+ }
383
+ return num > 0;
384
+ }
385
+ if (typeof prepared === "number") {
386
+ if (isNaN(prepared)) {
387
+ throw new BadRequestError("Cannot convert NaN to Boolean");
388
+ }
389
+ return prepared > 0;
390
+ }
391
+ throw new BadRequestError(`Cannot convert ${typeof prepared} to Boolean`);
392
+ }
393
+ /**
394
+ * Convert a value to a number
395
+ * - Arrays, objects, and JSON strings are unwrapped first
396
+ * - String "" becomes undefined
397
+ * - String "true" becomes 1
398
+ * - String "false" becomes 0
399
+ * - Strings that parse to numbers use those values
400
+ * - Strings that parse to NaN throw BadRequestError
401
+ * - Boolean true becomes 1, false becomes 0
402
+ * - Number passes through
403
+ */
404
+ function fabricNumber(value) {
405
+ // Prepare value by parsing JSON and unwrapping arrays/objects
406
+ const prepared = prepareForScalarConversion(value);
407
+ if (prepared === undefined || prepared === null) {
408
+ return undefined;
409
+ }
410
+ if (typeof prepared === "number") {
411
+ if (isNaN(prepared)) {
412
+ throw new BadRequestError("Cannot convert NaN to Number");
413
+ }
414
+ return prepared;
415
+ }
416
+ if (typeof prepared === "boolean") {
417
+ return prepared ? 1 : 0;
418
+ }
419
+ if (typeof prepared === "string") {
420
+ if (prepared === "") {
421
+ return undefined;
422
+ }
423
+ const lower = prepared.toLowerCase();
424
+ if (lower === "true") {
425
+ return 1;
426
+ }
427
+ if (lower === "false") {
428
+ return 0;
429
+ }
430
+ const num = parseFloat(prepared);
431
+ if (isNaN(num)) {
432
+ throw new BadRequestError(`Cannot convert "${prepared}" to Number`);
433
+ }
434
+ return num;
435
+ }
436
+ throw new BadRequestError(`Cannot convert ${typeof prepared} to Number`);
437
+ }
438
+ /**
439
+ * Convert a value to a string
440
+ * - Arrays, objects, and JSON strings are unwrapped first
441
+ * - String "" becomes undefined
442
+ * - Boolean true becomes "true", false becomes "false"
443
+ * - Number converts to string representation
444
+ * - String passes through
445
+ */
446
+ function fabricString(value) {
447
+ // Prepare value by parsing JSON and unwrapping arrays/objects
448
+ const prepared = prepareForScalarConversion(value);
449
+ if (prepared === undefined || prepared === null) {
450
+ return undefined;
451
+ }
452
+ if (typeof prepared === "string") {
453
+ if (prepared === "") {
454
+ return undefined;
455
+ }
456
+ return prepared;
457
+ }
458
+ if (typeof prepared === "boolean") {
459
+ return prepared ? "true" : "false";
460
+ }
461
+ if (typeof prepared === "number") {
462
+ if (isNaN(prepared)) {
463
+ throw new BadRequestError("Cannot convert NaN to String");
464
+ }
465
+ return String(prepared);
466
+ }
467
+ throw new BadRequestError(`Cannot convert ${typeof prepared} to String`);
468
+ }
469
+ /**
470
+ * Convert a value to an array
471
+ * - Non-arrays become arrays containing that value
472
+ * - Arrays of a single value become that value (unwrapped)
473
+ * - Multi-value arrays throw BadRequestError
474
+ * - undefined/null become undefined
475
+ */
476
+ function fabricArray(value) {
477
+ if (value === undefined || value === null) {
478
+ return undefined;
479
+ }
480
+ if (Array.isArray(value)) {
481
+ // Arrays pass through (single-element unwrapping happens when converting FROM array)
482
+ return value;
483
+ }
484
+ // Non-arrays become single-element arrays
485
+ return [value];
486
+ }
487
+ /**
488
+ * Convert a value to an object with a value property
489
+ * - Scalars become { value: scalar }
490
+ * - Arrays become { value: array }
491
+ * - Objects with a value attribute pass through
492
+ * - Objects without a value attribute throw BadRequestError
493
+ * - undefined/null become undefined
494
+ */
495
+ function fabricObject(value) {
496
+ if (value === undefined || value === null) {
497
+ return undefined;
498
+ }
499
+ // Check if already an object (but not an array)
500
+ if (typeof value === "object" && !Array.isArray(value)) {
501
+ const obj = value;
502
+ if ("value" in obj) {
503
+ return obj;
504
+ }
505
+ throw new BadRequestError("Object must have a value attribute");
506
+ }
507
+ // Scalars and arrays become { value: ... }
508
+ return { value };
509
+ }
510
+ /**
511
+ * Check if a type is a typed array (e.g., [String], [Number], [], etc.)
512
+ */
513
+ function isTypedArrayType(type) {
514
+ return Array.isArray(type);
515
+ }
516
+ /**
517
+ * Split a string on comma or tab delimiters for typed array conversion.
518
+ * Only splits if the string contains commas or tabs.
519
+ * Returns the original value if not a string or no delimiters found.
520
+ */
521
+ function splitStringForArray(value) {
522
+ if (typeof value !== "string") {
523
+ return value;
524
+ }
525
+ // Check for comma or tab delimiters
526
+ if (value.includes(",")) {
527
+ return value.split(",").map((s) => s.trim());
528
+ }
529
+ if (value.includes("\t")) {
530
+ return value.split("\t").map((s) => s.trim());
531
+ }
532
+ return value;
533
+ }
534
+ /**
535
+ * Try to parse a string as JSON for array context.
536
+ * Returns parsed value if it's an array, otherwise returns original.
537
+ */
538
+ function tryParseJsonArray(value) {
539
+ if (typeof value !== "string") {
540
+ return value;
541
+ }
542
+ const trimmed = value.trim();
543
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
544
+ try {
545
+ const parsed = JSON.parse(trimmed);
546
+ if (Array.isArray(parsed)) {
547
+ return parsed;
548
+ }
549
+ }
550
+ catch {
551
+ // Not valid JSON, fall through
552
+ }
553
+ }
554
+ return value;
555
+ }
556
+ /**
557
+ * Get the element type from a typed array type
558
+ * Returns undefined for untyped arrays ([])
559
+ */
560
+ function getArrayElementType(type) {
561
+ if (type.length === 0) {
562
+ return undefined; // Untyped array
563
+ }
564
+ const elementType = type[0];
565
+ // Handle constructor types
566
+ if (elementType === Boolean)
567
+ return "boolean";
568
+ if (elementType === Number)
569
+ return "number";
570
+ if (elementType === String)
571
+ return "string";
572
+ if (elementType === Object)
573
+ return "object";
574
+ // Handle string types
575
+ if (elementType === "boolean")
576
+ return "boolean";
577
+ if (elementType === "number")
578
+ return "number";
579
+ if (elementType === "string")
580
+ return "string";
581
+ if (elementType === "object")
582
+ return "object";
583
+ // Handle shorthand types
584
+ if (elementType === "")
585
+ return "string"; // "" shorthand for String
586
+ if (typeof elementType === "object" &&
587
+ elementType !== null &&
588
+ Object.keys(elementType).length === 0) {
589
+ return "object"; // {} shorthand for Object
590
+ }
591
+ throw new BadRequestError(`Unknown array element type: ${String(elementType)}`);
592
+ }
593
+ /**
594
+ * Convert a value to a typed array
595
+ * - Tries to parse JSON arrays first
596
+ * - Splits strings on comma/tab if present
597
+ * - Wraps non-arrays in an array
598
+ * - Converts each element to the specified element type
599
+ */
600
+ function fabricTypedArray(value, elementType) {
601
+ // Try to parse JSON array first
602
+ let processed = tryParseJsonArray(value);
603
+ // If still a string, try to split on comma/tab
604
+ processed = splitStringForArray(processed);
605
+ // Convert to array (wraps non-arrays)
606
+ const array = fabricArray(processed);
607
+ if (array === undefined) {
608
+ return undefined;
609
+ }
610
+ // If no element type specified, return as-is
611
+ if (elementType === undefined) {
612
+ return array;
613
+ }
614
+ // Convert each element to the element type
615
+ return array.map((element, index) => {
616
+ try {
617
+ switch (elementType) {
618
+ case "boolean":
619
+ return fabricBoolean(element);
620
+ case "number":
621
+ return fabricNumber(element);
622
+ case "object":
623
+ return fabricObject(element);
624
+ case "string":
625
+ return fabricString(element);
626
+ default:
627
+ throw new BadRequestError(`Unknown element type: ${elementType}`);
628
+ }
629
+ }
630
+ catch (error) {
631
+ if (error instanceof BadRequestError) {
632
+ throw new BadRequestError(`Cannot convert array element at index ${index}: ${error.message}`);
633
+ }
634
+ throw error;
635
+ }
636
+ });
637
+ }
638
+ /**
639
+ * Fabric a value to the specified type
640
+ */
641
+ function fabric(value, type) {
642
+ // Check for Date type first
643
+ if (isDateType(type)) {
644
+ return fabricDate(value);
645
+ }
646
+ // Check for typed array types
647
+ if (isTypedArrayType(type)) {
648
+ const elementType = getArrayElementType(type);
649
+ return fabricTypedArray(value, elementType);
650
+ }
651
+ const normalizedType = normalizeType(type);
652
+ switch (normalizedType) {
653
+ case "array":
654
+ return fabricArray(value);
655
+ case "boolean":
656
+ return fabricBoolean(value);
657
+ case "number":
658
+ return fabricNumber(value);
659
+ case "object":
660
+ return fabricObject(value);
661
+ case "string":
662
+ return fabricString(value);
663
+ default:
664
+ throw new BadRequestError(`Unknown type: ${String(type)}`);
665
+ }
666
+ }
667
+ /**
668
+ * Normalize type to string representation
669
+ */
670
+ function normalizeType(type) {
671
+ if (type === Array || type === "array") {
672
+ return "array";
673
+ }
674
+ if (type === Boolean || type === "boolean") {
675
+ return "boolean";
676
+ }
677
+ if (type === Number || type === "number") {
678
+ return "number";
679
+ }
680
+ if (type === Object || type === "object") {
681
+ return "object";
682
+ }
683
+ if (type === String || type === "string") {
684
+ return "string";
685
+ }
686
+ throw new BadRequestError(`Unknown type: ${String(type)}`);
687
+ }
688
+
689
+ // Service for @jaypie/fabric
690
+ /**
691
+ * Check if a single-element array is a typed array type constructor.
692
+ */
693
+ function isTypedArrayConstructor(element) {
694
+ return (element === Boolean ||
695
+ element === Number ||
696
+ element === String ||
697
+ element === Object ||
698
+ element === "boolean" ||
699
+ element === "number" ||
700
+ element === "string" ||
701
+ element === "object" ||
702
+ element === "" ||
703
+ (typeof element === "object" &&
704
+ element !== null &&
705
+ !(element instanceof RegExp) &&
706
+ Object.keys(element).length === 0));
707
+ }
708
+ /**
709
+ * Check if a type is a validated string type (array of string literals and/or RegExp).
710
+ * Distinguishes from typed arrays like [String], [Number], etc.
711
+ */
712
+ function isValidatedStringType(type) {
713
+ if (!Array.isArray(type)) {
714
+ return false;
715
+ }
716
+ // Empty array is untyped array, not validated string
717
+ if (type.length === 0) {
718
+ return false;
719
+ }
720
+ // Single-element arrays with type constructors are typed arrays
721
+ if (type.length === 1 && isTypedArrayConstructor(type[0])) {
722
+ return false;
723
+ }
724
+ // Check that all elements are strings or RegExp
725
+ return type.every((item) => typeof item === "string" || item instanceof RegExp);
726
+ }
727
+ /**
728
+ * Check if a type is a validated number type (array of number literals).
729
+ * Distinguishes from typed arrays like [Number], etc.
730
+ */
731
+ function isValidatedNumberType(type) {
732
+ if (!Array.isArray(type)) {
733
+ return false;
734
+ }
735
+ // Empty array is untyped array, not validated number
736
+ if (type.length === 0) {
737
+ return false;
738
+ }
739
+ // Single-element arrays with type constructors are typed arrays
740
+ if (type.length === 1 && isTypedArrayConstructor(type[0])) {
741
+ return false;
742
+ }
743
+ // Check that all elements are numbers
744
+ return type.every((item) => typeof item === "number");
745
+ }
746
+ /**
747
+ * Parse input string as JSON if it's a string
748
+ */
749
+ function parseInput(input) {
750
+ if (input === undefined || input === null) {
751
+ return {};
752
+ }
753
+ if (typeof input === "string") {
754
+ if (input === "") {
755
+ return {};
756
+ }
757
+ try {
758
+ const parsed = JSON.parse(input);
759
+ if (typeof parsed !== "object" ||
760
+ parsed === null ||
761
+ Array.isArray(parsed)) {
762
+ throw new BadRequestError("Input must be an object");
763
+ }
764
+ return parsed;
765
+ }
766
+ catch (error) {
767
+ if (error instanceof BadRequestError) {
768
+ throw error;
769
+ }
770
+ throw new BadRequestError("Invalid JSON input");
771
+ }
772
+ }
773
+ if (typeof input === "object" && !Array.isArray(input)) {
774
+ return input;
775
+ }
776
+ throw new BadRequestError("Input must be an object or JSON string");
777
+ }
778
+ /**
779
+ * Run validation on a value (supports async validators)
780
+ */
781
+ async function runValidation(value, validate, fieldName) {
782
+ if (typeof validate === "function") {
783
+ const result = await validate(value);
784
+ if (result === false) {
785
+ throw new BadRequestError(`Validation failed for field "${fieldName}"`);
786
+ }
787
+ }
788
+ else if (validate instanceof RegExp) {
789
+ if (typeof value !== "string" || !validate.test(value)) {
790
+ throw new BadRequestError(`Validation failed for field "${fieldName}"`);
791
+ }
792
+ }
793
+ else if (Array.isArray(validate)) {
794
+ // Check if value matches any item in the array
795
+ for (const item of validate) {
796
+ if (item instanceof RegExp) {
797
+ if (typeof value === "string" && item.test(value)) {
798
+ return; // Match found
799
+ }
800
+ }
801
+ else if (typeof item === "function") {
802
+ try {
803
+ const result = await item(value);
804
+ if (result !== false) {
805
+ return; // Match found
806
+ }
807
+ }
808
+ catch {
809
+ // Continue to next item
810
+ }
811
+ }
812
+ else if (value === item) {
813
+ return; // Scalar match found
814
+ }
815
+ }
816
+ throw new BadRequestError(`Validation failed for field "${fieldName}"`);
817
+ }
818
+ }
819
+ /**
820
+ * Check if a field is required
821
+ * A field is required unless it has a default OR required is explicitly false
822
+ */
823
+ function isFieldRequired(definition) {
824
+ if (definition.required === false) {
825
+ return false;
826
+ }
827
+ if (definition.default !== undefined) {
828
+ return false;
829
+ }
830
+ return true;
831
+ }
832
+ /**
833
+ * Process a single field through conversion and validation
834
+ */
835
+ async function processField(fieldName, value, definition) {
836
+ // Apply default if value is undefined
837
+ let processedValue = value;
838
+ if (processedValue === undefined && definition.default !== undefined) {
839
+ processedValue = definition.default;
840
+ }
841
+ // Determine actual type and validation
842
+ let actualType = definition.type;
843
+ let validation = definition.validate;
844
+ // Handle bare RegExp shorthand: /regex/
845
+ if (definition.type instanceof RegExp) {
846
+ actualType = String;
847
+ validation = definition.type; // The RegExp becomes the validation
848
+ }
849
+ // Handle validated string shorthand: ["value1", "value2"] or [/regex/]
850
+ else if (isValidatedStringType(definition.type)) {
851
+ actualType = String;
852
+ validation = definition.type; // The array becomes the validation
853
+ }
854
+ // Handle validated number shorthand: [1, 2, 3]
855
+ else if (isValidatedNumberType(definition.type)) {
856
+ actualType = Number;
857
+ validation = definition.type; // The array becomes the validation
858
+ }
859
+ // Fabric to target type
860
+ const convertedValue = fabric(processedValue, actualType);
861
+ // Check if required field is missing
862
+ if (convertedValue === undefined && isFieldRequired(definition)) {
863
+ throw new BadRequestError(`Missing required field "${fieldName}"`);
864
+ }
865
+ // Run validation if provided
866
+ if (validation !== undefined && convertedValue !== undefined) {
867
+ await runValidation(convertedValue, validation, fieldName);
868
+ }
869
+ return convertedValue;
870
+ }
871
+ /**
872
+ * Fabric a service function
873
+ *
874
+ * Service builds a function that initiates a "controller" step that:
875
+ * - Parses the input if it is a string to object
876
+ * - Fabrics each input field to its type
877
+ * - Calls the validation function or regular expression or checks the array
878
+ * - Calls the service function and returns the response
879
+ *
880
+ * The returned function has config properties for introspection.
881
+ */
882
+ function fabricService(config) {
883
+ const { input: inputDefinitions, service } = config;
884
+ const handler = async (rawInput, context) => {
885
+ // Parse input (handles string JSON)
886
+ const parsedInput = parseInput(rawInput);
887
+ // If no input definitions, pass through to service or return parsed input
888
+ if (!inputDefinitions) {
889
+ if (service) {
890
+ return service(parsedInput, context);
891
+ }
892
+ return parsedInput;
893
+ }
894
+ // Process all fields in parallel
895
+ const entries = Object.entries(inputDefinitions);
896
+ const processedValues = await Promise.all(entries.map(([fieldName, definition]) => processField(fieldName, parsedInput[fieldName], definition)));
897
+ // Build processed input object
898
+ const processedInput = {};
899
+ entries.forEach(([fieldName], index) => {
900
+ processedInput[fieldName] = processedValues[index];
901
+ });
902
+ // Return processed input if no service, otherwise call service
903
+ if (service) {
904
+ return service(processedInput, context);
905
+ }
906
+ return processedInput;
907
+ };
908
+ // Attach config properties directly to handler for flat access
909
+ const typedHandler = handler;
910
+ typedHandler.$fabric = FABRIC_VERSION;
911
+ if (config.alias !== undefined)
912
+ typedHandler.alias = config.alias;
913
+ if (config.description !== undefined)
914
+ typedHandler.description = config.description;
915
+ if (config.input !== undefined)
916
+ typedHandler.input = config.input;
917
+ if (config.service !== undefined)
918
+ typedHandler.service = config.service;
919
+ return typedHandler;
920
+ }
921
+
922
+ /**
923
+ * Extract token from Authorization header
924
+ * Removes "Bearer " prefix (case insensitive) and strips whitespace
925
+ *
926
+ * Examples:
927
+ * - "Bearer eyJhbGc..." → "eyJhbGc..."
928
+ * - "bearer eyJhbGc..." → "eyJhbGc..."
929
+ * - "BEARER eyJhbGc..." → "eyJhbGc..."
930
+ * - "eyJhbGc..." → "eyJhbGc..."
931
+ * - " eyJhbGc... " → "eyJhbGc..."
932
+ */
933
+ function extractToken(authHeader) {
934
+ if (!authHeader) {
935
+ return "";
936
+ }
937
+ let token = authHeader.trim();
938
+ // Remove "Bearer " prefix (case insensitive)
939
+ const bearerRegex = /^bearer\s+/i;
940
+ if (bearerRegex.test(token)) {
941
+ token = token.replace(bearerRegex, "");
942
+ }
943
+ return token.trim();
944
+ }
945
+ /**
946
+ * Get authorization header from Headers object
947
+ */
948
+ function getAuthHeader(headers) {
949
+ return headers.get("authorization");
950
+ }
951
+ /**
952
+ * Validate authorization and return auth context
953
+ *
954
+ * @param headers - Request headers
955
+ * @param config - Authorization configuration (function or false)
956
+ * @returns Auth context from the authorization function, or undefined if public
957
+ * @throws UnauthorizedError if authorization fails
958
+ */
959
+ async function validateAuthorization(headers, config) {
960
+ // Public endpoint - no authorization required
961
+ if (config === false) {
962
+ return undefined;
963
+ }
964
+ const authHeader = getAuthHeader(headers);
965
+ const token = extractToken(authHeader);
966
+ // If authorization is required but no token provided
967
+ if (!token) {
968
+ throw new UnauthorizedError("Authorization header required");
969
+ }
970
+ // Call the authorization function
971
+ const authFunction = config;
972
+ return authFunction(token);
973
+ }
974
+
975
+ /**
976
+ * Default HTTP transformation function
977
+ * Merges query parameters with body (body takes precedence)
978
+ */
979
+ const defaultHttpTransform = ({ body, query, }) => {
980
+ const queryObject = Object.fromEntries(query.entries());
981
+ const bodyObject = typeof body === "object" && body !== null ? body : {};
982
+ return {
983
+ ...queryObject,
984
+ ...bodyObject,
985
+ };
986
+ };
987
+
988
+ /**
989
+ * Check if a value is a fabricService (has $fabric property)
990
+ */
991
+ function isFabricService(value) {
992
+ return (typeof value === "function" &&
993
+ "$fabric" in value &&
994
+ typeof value.$fabric === "string");
995
+ }
996
+ /**
997
+ * Create an HTTP-aware fabric service
998
+ *
999
+ * Extends fabricService with:
1000
+ * - HTTP context transformation (body, headers, method, path, query, params)
1001
+ * - Authorization handling (token extraction from Authorization header)
1002
+ * - CORS configuration (enabled by default)
1003
+ *
1004
+ * Accepts either:
1005
+ * - Inline service definition (with `service` function)
1006
+ * - Pre-built `fabricService` instance (via `service` property)
1007
+ */
1008
+ function fabricHttp(config) {
1009
+ const { authorization = false, cors = true, http = defaultHttpTransform, service: serviceConfig, stream = false, ...baseConfig } = config;
1010
+ // Resolve the underlying service
1011
+ let underlyingService;
1012
+ if (isFabricService(serviceConfig)) {
1013
+ // Pre-built fabricService - merge configs
1014
+ underlyingService = serviceConfig;
1015
+ // Merge base config properties from the pre-built service
1016
+ if (baseConfig.alias === undefined &&
1017
+ underlyingService.alias !== undefined) {
1018
+ baseConfig.alias = underlyingService.alias;
1019
+ }
1020
+ if (baseConfig.description === undefined &&
1021
+ underlyingService.description !== undefined) {
1022
+ baseConfig.description = underlyingService.description;
1023
+ }
1024
+ if (baseConfig.input === undefined &&
1025
+ underlyingService.input !== undefined) {
1026
+ baseConfig.input = underlyingService.input;
1027
+ }
1028
+ }
1029
+ else {
1030
+ // Inline service definition or plain function
1031
+ const serviceFunction = serviceConfig;
1032
+ underlyingService = fabricService({
1033
+ ...baseConfig,
1034
+ service: serviceFunction,
1035
+ });
1036
+ }
1037
+ // Create the HTTP handler that processes HTTP context
1038
+ const httpHandler = async (input, context) => {
1039
+ // If context has HTTP info, process authorization
1040
+ // (HTTP context is added by the adapter layer like fabricExpress)
1041
+ if (context?.http && authorization !== false) {
1042
+ const authResult = await validateAuthorization(context.http.headers, authorization);
1043
+ // Add auth result to context
1044
+ context.auth = authResult;
1045
+ }
1046
+ // Call the underlying service
1047
+ return underlyingService(input, context);
1048
+ };
1049
+ // Create the HTTP service with all properties
1050
+ const httpService = httpHandler;
1051
+ // Copy properties from config (which may have been merged with underlying service)
1052
+ httpService.$fabric = underlyingService.$fabric;
1053
+ // Use baseConfig values (which include overrides) or fall back to underlying service
1054
+ const resolvedAlias = baseConfig.alias ?? underlyingService.alias;
1055
+ const resolvedDescription = baseConfig.description ?? underlyingService.description;
1056
+ const resolvedInput = baseConfig.input ?? underlyingService.input;
1057
+ if (resolvedAlias !== undefined) {
1058
+ httpService.alias = resolvedAlias;
1059
+ }
1060
+ if (resolvedDescription !== undefined) {
1061
+ httpService.description = resolvedDescription;
1062
+ }
1063
+ if (resolvedInput !== undefined) {
1064
+ httpService.input = resolvedInput;
1065
+ }
1066
+ if (underlyingService.service !== undefined) {
1067
+ httpService.service = underlyingService.service;
1068
+ }
1069
+ // Add HTTP-specific properties
1070
+ httpService.authorization = authorization;
1071
+ httpService.cors = cors;
1072
+ httpService.http = http;
1073
+ httpService.stream = stream;
1074
+ return httpService;
1075
+ }
1076
+
1077
+ /**
1078
+ * Create the "archive" service for a FabricData endpoint
1079
+ * POST /{model}/:id/archive - Archive an entity
1080
+ */
1081
+ function createArchiveService(modelConfig, operationConfig, globalConfig) {
1082
+ const { alias, name } = modelConfig;
1083
+ return fabricHttp({
1084
+ alias: `archive-${alias}`,
1085
+ description: `Archive a ${name}`,
1086
+ input: {
1087
+ id: { type: String, description: `${name} ID` },
1088
+ },
1089
+ authorization: operationConfig.authorization ?? globalConfig.authorization,
1090
+ cors: globalConfig.cors,
1091
+ http: operationConfig.http ?? transformArchive,
1092
+ service: async (input) => {
1093
+ // Dynamically import DynamoDB utilities
1094
+ const { archiveEntity, getEntity } = await import('@jaypie/dynamodb');
1095
+ const id = input.id;
1096
+ if (!id) {
1097
+ throw new BadRequestError("ID is required");
1098
+ }
1099
+ // Check if entity exists
1100
+ const existing = await getEntity({ id, model: alias });
1101
+ if (!existing) {
1102
+ throw new NotFoundError(`${name} not found`);
1103
+ }
1104
+ // Archive the entity
1105
+ const archived = await archiveEntity({ id, model: alias });
1106
+ if (!archived) {
1107
+ throw new NotFoundError(`${name} not found`);
1108
+ }
1109
+ // Fetch the updated entity to return
1110
+ const updated = await getEntity({ id, model: alias });
1111
+ return updated;
1112
+ },
1113
+ });
1114
+ }
1115
+
1116
+ /**
1117
+ * Create the "create" service for a FabricData endpoint
1118
+ * POST /{model} - Create a new entity
1119
+ */
1120
+ function createCreateService(modelConfig, operationConfig, globalConfig) {
1121
+ const { alias, name } = modelConfig;
1122
+ return fabricHttp({
1123
+ alias: `create-${alias}`,
1124
+ description: `Create a new ${name}`,
1125
+ authorization: operationConfig.authorization ?? globalConfig.authorization,
1126
+ cors: globalConfig.cors,
1127
+ http: operationConfig.http ?? transformCreate,
1128
+ service: async (input, context) => {
1129
+ // Dynamically import DynamoDB utilities
1130
+ const { putEntity } = await import('@jaypie/dynamodb');
1131
+ // Calculate scope
1132
+ const scopeConfig = globalConfig.scope;
1133
+ const httpContext = context?.http;
1134
+ const scope = httpContext
1135
+ ? await calculateScopeFromConfig(scopeConfig, httpContext)
1136
+ : "@";
1137
+ // Validate required fields
1138
+ if (!input || typeof input !== "object") {
1139
+ throw new BadRequestError("Request body is required");
1140
+ }
1141
+ // Apply transform if configured
1142
+ let entityInput = input;
1143
+ if (operationConfig.transform) {
1144
+ entityInput = {
1145
+ ...entityInput,
1146
+ ...operationConfig.transform(entityInput, null),
1147
+ };
1148
+ }
1149
+ // Build the entity
1150
+ const now = new Date().toISOString();
1151
+ const entity = {
1152
+ ...entityInput,
1153
+ createdAt: now,
1154
+ id: crypto.randomUUID(),
1155
+ model: alias,
1156
+ name: entityInput.name ?? name,
1157
+ scope,
1158
+ sequence: Date.now(),
1159
+ updatedAt: now,
1160
+ };
1161
+ // Create the entity
1162
+ const created = await putEntity({ entity });
1163
+ return created;
1164
+ },
1165
+ });
1166
+ }
1167
+
1168
+ /**
1169
+ * Create the "delete" service for a FabricData endpoint
1170
+ * DELETE /{model}/:id - Soft delete an entity
1171
+ */
1172
+ function createDeleteService(modelConfig, operationConfig, globalConfig) {
1173
+ const { alias, name } = modelConfig;
1174
+ return fabricHttp({
1175
+ alias: `delete-${alias}`,
1176
+ description: `Delete a ${name}`,
1177
+ input: {
1178
+ id: { type: String, description: `${name} ID` },
1179
+ },
1180
+ authorization: operationConfig.authorization ?? globalConfig.authorization,
1181
+ cors: globalConfig.cors,
1182
+ http: operationConfig.http ?? transformDelete,
1183
+ service: async (input) => {
1184
+ // Dynamically import DynamoDB utilities
1185
+ const { deleteEntity, getEntity } = await import('@jaypie/dynamodb');
1186
+ const id = input.id;
1187
+ if (!id) {
1188
+ throw new BadRequestError("ID is required");
1189
+ }
1190
+ // Check if entity exists
1191
+ const existing = await getEntity({ id, model: alias });
1192
+ if (!existing) {
1193
+ throw new NotFoundError(`${name} not found`);
1194
+ }
1195
+ // Soft delete the entity
1196
+ const deleted = await deleteEntity({ id, model: alias });
1197
+ if (!deleted) {
1198
+ throw new NotFoundError(`${name} not found`);
1199
+ }
1200
+ // Return success (no content)
1201
+ return null;
1202
+ },
1203
+ });
1204
+ }
1205
+
1206
+ /**
1207
+ * Create an "execute" service for a custom action
1208
+ * POST /{model}/:id/{alias} - Execute a custom action on an entity
1209
+ */
1210
+ function createExecuteService(modelConfig, executeConfig, globalConfig) {
1211
+ const { alias: modelAlias, name: modelName } = modelConfig;
1212
+ const { alias: actionAlias, authorization, description, input: inputDefinition, service: actionService, } = executeConfig;
1213
+ return fabricHttp({
1214
+ alias: `${modelAlias}-${actionAlias}`,
1215
+ description: description ?? `${actionAlias} action on ${modelName}`,
1216
+ input: {
1217
+ id: { type: String, description: `${modelName} ID` },
1218
+ ...inputDefinition,
1219
+ },
1220
+ authorization: authorization ?? globalConfig.authorization,
1221
+ cors: globalConfig.cors,
1222
+ http: transformExecute,
1223
+ service: async (input) => {
1224
+ // Dynamically import DynamoDB utilities
1225
+ const { getEntity } = await import('@jaypie/dynamodb');
1226
+ const id = input.id;
1227
+ const { id: _id, ...actionInput } = input;
1228
+ if (!id) {
1229
+ throw new BadRequestError("ID is required");
1230
+ }
1231
+ // Fetch the entity
1232
+ const entity = await getEntity({ id, model: modelAlias });
1233
+ if (!entity) {
1234
+ throw new NotFoundError(`${modelName} not found`);
1235
+ }
1236
+ // Execute the action
1237
+ const result = await actionService(entity, actionInput);
1238
+ return result;
1239
+ },
1240
+ });
1241
+ }
1242
+
1243
+ /**
1244
+ * Create the "list" service for a FabricData endpoint
1245
+ * GET /{model} - List entities with pagination
1246
+ */
1247
+ function createListService(modelConfig, operationConfig, globalConfig) {
1248
+ const { alias, name, pluralAlias } = modelConfig;
1249
+ const defaultLimit = globalConfig.defaultLimit ?? DEFAULT_LIMIT;
1250
+ const maxLimit = globalConfig.maxLimit ?? MAX_LIMIT;
1251
+ // Build the HTTP transform function
1252
+ const httpTransform = operationConfig.http ??
1253
+ ((context) => transformList(context, defaultLimit, maxLimit));
1254
+ return fabricHttp({
1255
+ alias: `list-${pluralAlias}`,
1256
+ description: `List ${name} entities`,
1257
+ input: {
1258
+ archived: {
1259
+ type: Boolean,
1260
+ default: false,
1261
+ required: false,
1262
+ description: "Include archived entities",
1263
+ },
1264
+ ascending: {
1265
+ type: Boolean,
1266
+ default: false,
1267
+ required: false,
1268
+ description: "Sort ascending by sequence",
1269
+ },
1270
+ cursor: {
1271
+ type: String,
1272
+ required: false,
1273
+ description: "Pagination cursor",
1274
+ },
1275
+ deleted: {
1276
+ type: Boolean,
1277
+ default: false,
1278
+ required: false,
1279
+ description: "Include deleted entities",
1280
+ },
1281
+ limit: {
1282
+ type: Number,
1283
+ default: defaultLimit,
1284
+ required: false,
1285
+ description: `Number of items per page (max: ${maxLimit})`,
1286
+ },
1287
+ },
1288
+ authorization: operationConfig.authorization ?? globalConfig.authorization,
1289
+ cors: globalConfig.cors,
1290
+ http: httpTransform,
1291
+ service: async (input, context) => {
1292
+ // Dynamically import DynamoDB utilities
1293
+ const { queryByScope } = await import('@jaypie/dynamodb');
1294
+ // Calculate scope
1295
+ const scopeConfig = globalConfig.scope;
1296
+ const httpContext = context?.http;
1297
+ const scope = httpContext
1298
+ ? await calculateScopeFromConfig(scopeConfig, httpContext)
1299
+ : "@";
1300
+ // Parse input with defaults
1301
+ const archived = input.archived ?? false;
1302
+ const ascending = input.ascending ?? false;
1303
+ const deleted = input.deleted ?? false;
1304
+ const limit = Math.min(input.limit ?? defaultLimit, maxLimit);
1305
+ const startKey = decodeCursor(input.cursor ?? input.startKey);
1306
+ // Query entities
1307
+ const result = await queryByScope({
1308
+ archived,
1309
+ ascending,
1310
+ deleted,
1311
+ limit,
1312
+ model: alias,
1313
+ scope,
1314
+ startKey,
1315
+ });
1316
+ // Build response
1317
+ const response = {
1318
+ items: result.items,
1319
+ nextKey: encodeCursor(result.lastEvaluatedKey),
1320
+ };
1321
+ return response;
1322
+ },
1323
+ });
1324
+ }
1325
+
1326
+ /**
1327
+ * Create the "read" service for a FabricData endpoint
1328
+ * GET /{model}/:id - Get a single entity by ID
1329
+ */
1330
+ function createReadService(modelConfig, operationConfig, globalConfig) {
1331
+ const { alias, name } = modelConfig;
1332
+ return fabricHttp({
1333
+ alias: `read-${alias}`,
1334
+ description: `Get a ${name} by ID`,
1335
+ input: {
1336
+ id: { type: String, description: `${name} ID` },
1337
+ },
1338
+ authorization: operationConfig.authorization ?? globalConfig.authorization,
1339
+ cors: globalConfig.cors,
1340
+ http: operationConfig.http ?? transformRead,
1341
+ service: async (input) => {
1342
+ // Dynamically import DynamoDB utilities
1343
+ const { getEntity } = await import('@jaypie/dynamodb');
1344
+ const id = input.id;
1345
+ if (!id) {
1346
+ throw new BadRequestError("ID is required");
1347
+ }
1348
+ // Fetch the entity
1349
+ const entity = await getEntity({ id, model: alias });
1350
+ if (!entity) {
1351
+ throw new NotFoundError(`${name} not found`);
1352
+ }
1353
+ return entity;
1354
+ },
1355
+ });
1356
+ }
1357
+
1358
+ /**
1359
+ * Create the "update" service for a FabricData endpoint
1360
+ * POST /{model}/:id - Update an existing entity
1361
+ */
1362
+ function createUpdateService(modelConfig, operationConfig, globalConfig) {
1363
+ const { alias, name } = modelConfig;
1364
+ return fabricHttp({
1365
+ alias: `update-${alias}`,
1366
+ description: `Update a ${name}`,
1367
+ input: {
1368
+ id: { type: String, description: `${name} ID` },
1369
+ },
1370
+ authorization: operationConfig.authorization ?? globalConfig.authorization,
1371
+ cors: globalConfig.cors,
1372
+ http: operationConfig.http ?? transformUpdate,
1373
+ service: async (input) => {
1374
+ // Dynamically import DynamoDB utilities
1375
+ const { getEntity, updateEntity } = await import('@jaypie/dynamodb');
1376
+ const id = input.id;
1377
+ const { id: _id, ...updateData } = input;
1378
+ if (!id) {
1379
+ throw new BadRequestError("ID is required");
1380
+ }
1381
+ // Fetch existing entity
1382
+ const existing = await getEntity({ id, model: alias });
1383
+ if (!existing) {
1384
+ throw new NotFoundError(`${name} not found`);
1385
+ }
1386
+ // Apply transform if configured
1387
+ let entityUpdate = updateData;
1388
+ if (operationConfig.transform) {
1389
+ entityUpdate = {
1390
+ ...entityUpdate,
1391
+ ...operationConfig.transform(updateData, existing),
1392
+ };
1393
+ }
1394
+ // Build the updated entity
1395
+ const entity = {
1396
+ ...existing,
1397
+ ...entityUpdate,
1398
+ // Preserve immutable fields
1399
+ createdAt: existing.createdAt,
1400
+ id: existing.id,
1401
+ model: existing.model,
1402
+ scope: existing.scope,
1403
+ sequence: existing.sequence,
1404
+ };
1405
+ // Update the entity
1406
+ const updated = await updateEntity({ entity });
1407
+ return updated;
1408
+ },
1409
+ });
1410
+ }
1411
+
1412
+ /**
1413
+ * Resolve model configuration from string or object
1414
+ */
1415
+ function resolveModelConfig(model) {
1416
+ if (typeof model === "string") {
1417
+ return {
1418
+ alias: model,
1419
+ description: undefined,
1420
+ name: capitalize(model),
1421
+ pluralAlias: pluralize(model),
1422
+ };
1423
+ }
1424
+ return {
1425
+ alias: model.alias,
1426
+ description: model.description,
1427
+ name: model.name ?? capitalize(model.alias),
1428
+ pluralAlias: pluralize(model.alias),
1429
+ };
1430
+ }
1431
+ /**
1432
+ * Resolve operation configuration from boolean or object
1433
+ */
1434
+ function resolveOperationConfig(option, defaultEnabled = true) {
1435
+ // Undefined means use defaults (enabled)
1436
+ if (option === undefined) {
1437
+ return { enabled: defaultEnabled };
1438
+ }
1439
+ // Boolean means enabled/disabled with defaults
1440
+ if (typeof option === "boolean") {
1441
+ return { enabled: option };
1442
+ }
1443
+ // Object config
1444
+ return {
1445
+ authorization: option.authorization,
1446
+ enabled: option.enabled !== false,
1447
+ http: option.http,
1448
+ transform: option.transform,
1449
+ };
1450
+ }
1451
+ /**
1452
+ * Create CRUD HTTP services for a Jaypie model backed by DynamoDB
1453
+ *
1454
+ * Generates standard operations:
1455
+ * - POST /{model} - Create
1456
+ * - GET /{model} - List
1457
+ * - GET /{model}/:id - Read
1458
+ * - POST /{model}/:id - Update
1459
+ * - DELETE /{model}/:id - Delete
1460
+ * - POST /{model}/:id/archive - Archive
1461
+ * - POST /{model}/:id/{action} - Custom execute actions
1462
+ *
1463
+ * @example
1464
+ * ```typescript
1465
+ * // Basic usage
1466
+ * const recordServices = FabricData({ model: "record" });
1467
+ *
1468
+ * // With authorization
1469
+ * const recordServices = FabricData({
1470
+ * model: "record",
1471
+ * authorization: validateToken,
1472
+ * operations: {
1473
+ * read: { authorization: false }, // Public read
1474
+ * delete: { authorization: requireAdmin },
1475
+ * archive: false, // Disabled
1476
+ * },
1477
+ * });
1478
+ *
1479
+ * // Use with FabricHttpServer
1480
+ * const server = new FabricHttpServer({
1481
+ * services: recordServices.services,
1482
+ * prefix: "/api",
1483
+ * });
1484
+ * ```
1485
+ */
1486
+ function FabricData(config) {
1487
+ const modelConfig = resolveModelConfig(config.model);
1488
+ const { alias, pluralAlias } = modelConfig;
1489
+ const services = [];
1490
+ // Resolve operation configs
1491
+ const operations = config.operations ?? {};
1492
+ // Create operation
1493
+ const createOp = resolveOperationConfig(operations.create);
1494
+ if (createOp.enabled) {
1495
+ services.push(createCreateService(modelConfig, createOp, config));
1496
+ }
1497
+ // List operation
1498
+ const listOp = resolveOperationConfig(operations.list);
1499
+ if (listOp.enabled) {
1500
+ services.push(createListService(modelConfig, listOp, config));
1501
+ }
1502
+ // Read operation
1503
+ const readOp = resolveOperationConfig(operations.read);
1504
+ if (readOp.enabled) {
1505
+ services.push(createReadService(modelConfig, readOp, config));
1506
+ }
1507
+ // Update operation
1508
+ const updateOp = resolveOperationConfig(operations.update);
1509
+ if (updateOp.enabled) {
1510
+ services.push(createUpdateService(modelConfig, updateOp, config));
1511
+ }
1512
+ // Delete operation
1513
+ const deleteOp = resolveOperationConfig(operations.delete);
1514
+ if (deleteOp.enabled) {
1515
+ services.push(createDeleteService(modelConfig, deleteOp, config));
1516
+ }
1517
+ // Archive operation
1518
+ const archiveOp = resolveOperationConfig(operations.archive);
1519
+ if (archiveOp.enabled) {
1520
+ services.push(createArchiveService(modelConfig, archiveOp, config));
1521
+ }
1522
+ // Execute actions
1523
+ if (config.execute) {
1524
+ for (const executeConfig of config.execute) {
1525
+ services.push(createExecuteService(modelConfig, executeConfig, config));
1526
+ }
1527
+ }
1528
+ return {
1529
+ model: alias,
1530
+ prefix: `/${pluralAlias}`,
1531
+ services,
1532
+ };
1533
+ }
1534
+ /**
1535
+ * Check if a value is a FabricDataResult
1536
+ */
1537
+ function isFabricDataResult(value) {
1538
+ if (typeof value !== "object" || value === null) {
1539
+ return false;
1540
+ }
1541
+ const obj = value;
1542
+ return (typeof obj.model === "string" &&
1543
+ typeof obj.prefix === "string" &&
1544
+ Array.isArray(obj.services));
1545
+ }
1546
+
1547
+ export { APEX, DEFAULT_LIMIT, FabricData, MAX_LIMIT, calculateScopeFromConfig, capitalize, createArchiveService, createCreateService, createDeleteService, createExecuteService, createListService, createReadService, createUpdateService, decodeCursor, encodeCursor, extractId, extractScopeContext, isFabricDataResult, pluralize, transformArchive, transformCreate, transformDelete, transformExecute, transformList, transformRead, transformUpdate };
1548
+ //# sourceMappingURL=index.js.map