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