@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
package/README.md ADDED
@@ -0,0 +1,677 @@
1
+ # @jaypie/fabric
2
+
3
+ Jaypie modeling framework with type conversion, service handlers, and adapters for CLI, Lambda, LLM, and MCP.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @jaypie/fabric
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### fabricService
14
+
15
+ Create validated service endpoints with automatic type conversion:
16
+
17
+ ```typescript
18
+ import { fabricService } from "@jaypie/fabric";
19
+
20
+ const divisionHandler = fabricService({
21
+ alias: "division",
22
+ description: "Divides two numbers",
23
+ input: {
24
+ numerator: {
25
+ default: 12,
26
+ description: "Number 'on top', which is to be divided",
27
+ type: Number,
28
+ },
29
+ denominator: {
30
+ default: 3,
31
+ description: "Number 'on bottom', how many ways to split the value",
32
+ type: Number,
33
+ validate: (value) => value !== 0,
34
+ }
35
+ },
36
+ service: ({ numerator, denominator }) => (numerator / denominator),
37
+ });
38
+
39
+ await divisionHandler(); // =4
40
+ await divisionHandler({ numerator: 24 }); // =8
41
+ await divisionHandler({ numerator: 24, denominator: 2 }); // =12
42
+ await divisionHandler({ numerator: "14", denominator: "7" }); // =2
43
+ await divisionHandler({ numerator: 1, denominator: 0 }); // throws BadRequestError(); does not validate
44
+ await divisionHandler('{ "numerator": "18" }'); // =3; String parses as JSON
45
+ ```
46
+
47
+ ### Type Conversion (Fabric Functions)
48
+
49
+ ```typescript
50
+ import { fabric, fabricNumber, fabricBoolean, fabricString } from "@jaypie/fabric";
51
+
52
+ fabricBoolean("true"); // true
53
+ fabricBoolean(1); // true
54
+ fabricNumber("42"); // 42
55
+ fabricNumber(true); // 1
56
+ fabricString(true); // "true"
57
+ fabricString(42); // "42"
58
+ ```
59
+
60
+ ### Commander Adapter
61
+
62
+ ```typescript
63
+ import { Command } from "commander";
64
+ import { fabricService } from "@jaypie/fabric";
65
+ import { fabricCommand } from "@jaypie/fabric/commander";
66
+
67
+ const handler = fabricService({
68
+ alias: "greet",
69
+ description: "Greet a user",
70
+ input: {
71
+ userName: { type: String, flag: "user", letter: "u" },
72
+ loud: { type: Boolean, letter: "l", default: false },
73
+ },
74
+ service: ({ loud, userName }) => {
75
+ const greeting = `Hello, ${userName}!`;
76
+ return loud ? greeting.toUpperCase() : greeting;
77
+ },
78
+ });
79
+
80
+ const program = new Command();
81
+ fabricCommand({ service: handler, program });
82
+ program.parse();
83
+ // Usage: greet --user Alice -l
84
+ ```
85
+
86
+ ### Lambda Adapter
87
+
88
+ ```typescript
89
+ import { fabricService } from "@jaypie/fabric";
90
+ import { fabricLambda } from "@jaypie/fabric/lambda";
91
+
92
+ const evaluationsHandler = fabricService({
93
+ alias: "evaluationsHandler",
94
+ input: {
95
+ count: { type: Number, default: 1 },
96
+ models: { type: [String], default: [] },
97
+ plan: { type: String },
98
+ },
99
+ service: ({ count, models, plan }) => ({
100
+ jobId: `job-${Date.now()}`,
101
+ plan,
102
+ }),
103
+ });
104
+
105
+ export const handler = fabricLambda(evaluationsHandler, {
106
+ secrets: ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
107
+ });
108
+ ```
109
+
110
+ ### LLM Adapter
111
+
112
+ ```typescript
113
+ import { fabricService } from "@jaypie/fabric";
114
+ import { fabricTool } from "@jaypie/fabric/llm";
115
+ import { Toolkit } from "@jaypie/llm";
116
+
117
+ const handler = fabricService({
118
+ alias: "greet",
119
+ description: "Greet a user by name",
120
+ input: {
121
+ userName: { type: String, description: "The user's name" },
122
+ loud: { type: Boolean, default: false, description: "Shout the greeting" },
123
+ },
124
+ service: ({ userName, loud }) => {
125
+ const greeting = `Hello, ${userName}!`;
126
+ return loud ? greeting.toUpperCase() : greeting;
127
+ },
128
+ });
129
+
130
+ const { tool } = fabricTool({ service: handler });
131
+ const toolkit = new Toolkit([tool]);
132
+ ```
133
+
134
+ ### MCP Adapter
135
+
136
+ ```typescript
137
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
138
+ import { fabricService } from "@jaypie/fabric";
139
+ import { fabricMcp } from "@jaypie/fabric/mcp";
140
+
141
+ const handler = fabricService({
142
+ alias: "greet",
143
+ description: "Greet a user by name",
144
+ input: {
145
+ userName: { type: String, description: "The user's name" },
146
+ loud: { type: Boolean, default: false, description: "Shout the greeting" },
147
+ },
148
+ service: ({ userName, loud }) => {
149
+ const greeting = `Hello, ${userName}!`;
150
+ return loud ? greeting.toUpperCase() : greeting;
151
+ },
152
+ });
153
+
154
+ const server = new McpServer({ name: "my-server", version: "1.0.0" });
155
+ fabricMcp({ service: handler, server });
156
+ ```
157
+
158
+ ### Express Adapter
159
+
160
+ ```typescript
161
+ import { Router } from "express";
162
+ import { fabricHttp } from "@jaypie/fabric/http";
163
+ import { fabricExpress, FabricRouter } from "@jaypie/fabric/express";
164
+
165
+ // Create a fabricHttp service
166
+ const userService = fabricHttp({
167
+ alias: "users",
168
+ input: {
169
+ id: { type: String, required: false },
170
+ },
171
+ service: ({ id }) => id ? getUser(id) : listUsers(),
172
+ });
173
+
174
+ // Option 1: Single service as middleware
175
+ const middleware = fabricExpress({ service: userService });
176
+ router.use("/api", middleware);
177
+ // Routes: GET/POST/DELETE /api/users
178
+
179
+ // Option 2: Multiple services with FabricRouter
180
+ const productService = fabricHttp({
181
+ alias: "products",
182
+ service: () => listProducts(),
183
+ });
184
+
185
+ const router = FabricRouter({
186
+ services: [
187
+ userService,
188
+ productService,
189
+ {
190
+ service: userService,
191
+ path: "/users/:id",
192
+ methods: ["GET", "PUT", "DELETE"],
193
+ },
194
+ ],
195
+ });
196
+
197
+ app.use("/v1", router);
198
+ // Routes: /v1/users, /v1/products, /v1/users/:id
199
+ ```
200
+
201
+ ### HTTP Adapter
202
+
203
+ Create HTTP-aware services with built-in authorization and CORS support:
204
+
205
+ ```typescript
206
+ import { fabricService } from "@jaypie/fabric";
207
+ import { fabricHttp } from "@jaypie/fabric/http";
208
+
209
+ // Inline service definition
210
+ const userService = fabricHttp({
211
+ alias: "users",
212
+ description: "User management API",
213
+ input: {
214
+ id: { type: String },
215
+ name: { type: String, required: false },
216
+ },
217
+ // Authorization: function receives token from Authorization header
218
+ // (Bearer prefix removed, whitespace stripped)
219
+ authorization: async (token) => {
220
+ const user = await validateJwt(token);
221
+ if (!user) throw new UnauthorizedError();
222
+ return user; // Available in context.auth
223
+ },
224
+ // CORS enabled by default, customize as needed
225
+ cors: {
226
+ origin: ["https://app.example.com"],
227
+ credentials: true,
228
+ },
229
+ service: ({ id, name }, context) => {
230
+ console.log("Authenticated user:", context.auth);
231
+ return { id, name };
232
+ },
233
+ });
234
+
235
+ // Or wrap an existing fabricService
236
+ const coreService = fabricService({
237
+ alias: "division",
238
+ input: {
239
+ numerator: { type: Number },
240
+ denominator: { type: Number },
241
+ },
242
+ service: ({ numerator, denominator }) => numerator / denominator,
243
+ });
244
+
245
+ const divisionApi = fabricHttp({
246
+ service: coreService,
247
+ authorization: false, // Public endpoint
248
+ });
249
+ ```
250
+
251
+ #### HTTP Transformation
252
+
253
+ Customize how HTTP context maps to service input:
254
+
255
+ ```typescript
256
+ const customService = fabricHttp({
257
+ alias: "custom",
258
+ input: {
259
+ userId: { type: String },
260
+ action: { type: String },
261
+ },
262
+ // Transform HTTP context to service input
263
+ http: ({ headers, params, body }) => ({
264
+ userId: headers.get("x-user-id") ?? params.userId,
265
+ action: body.action,
266
+ }),
267
+ service: ({ userId, action }) => performAction(userId, action),
268
+ });
269
+ ```
270
+
271
+ #### HTTP Streaming
272
+
273
+ Enable NDJSON streaming for long-running tasks or LLM responses:
274
+
275
+ ```typescript
276
+ import { fabricHttp, pipeLlmStream } from "@jaypie/fabric/http";
277
+ import Llm from "@jaypie/llm";
278
+
279
+ const streamingService = fabricHttp({
280
+ alias: "chat",
281
+ input: { message: { type: String } },
282
+ stream: true, // Enable NDJSON streaming
283
+ service: async function* ({ message }, context) {
284
+ // Send progress messages (streamed as message events)
285
+ context.sendMessage({ content: "Processing...", level: "info" });
286
+
287
+ // Stream LLM response
288
+ const llmStream = Llm.stream(message);
289
+ yield* pipeLlmStream(llmStream);
290
+ },
291
+ });
292
+ ```
293
+
294
+ Stream events use NDJSON format with `stream` as the discriminator field:
295
+
296
+ ```json
297
+ {"stream":"message","content":"Processing...","level":"info"}
298
+ {"stream":"text","content":"Hello"}
299
+ {"stream":"tool_call","toolCall":{"id":"...","name":"...","arguments":"..."}}
300
+ {"stream":"tool_result","toolResult":{"id":"...","name":"...","result":"..."}}
301
+ {"stream":"data","data":{"result":42}}
302
+ {"stream":"error","error":{"status":500,"title":"Error"}}
303
+ {"stream":"noop"}
304
+ {"stream":"complete"}
305
+ ```
306
+
307
+ Streaming utilities:
308
+ - `pipeLlmStream(llmStream)` - Convert @jaypie/llm stream to HTTP events
309
+ - `createStreamContext(writer)` - Create context with `streamText()` and `streamEvent()` methods
310
+ - `createCompleteEvent()` - Create stream completion event
311
+ - `createNoopEvent()` - Create keep-alive signal (empty event)
312
+ - `formatNdjsonEvent(event)` / `formatSseEvent(event)` - Format events for output
313
+
314
+ #### FabricHttpServer (Standalone Lambda)
315
+
316
+ Route multiple services in a single Lambda function without Express:
317
+
318
+ ```typescript
319
+ import { fabricHttp, FabricHttpServer } from "@jaypie/fabric/http";
320
+ import { lambdaHandler } from "@jaypie/lambda";
321
+
322
+ // Create HTTP services
323
+ const userService = fabricHttp({
324
+ alias: "users",
325
+ input: { id: { type: String, required: false } },
326
+ service: ({ id }) => id ? getUser(id) : listUsers(),
327
+ });
328
+
329
+ const productService = fabricHttp({
330
+ alias: "products",
331
+ service: () => listProducts(),
332
+ });
333
+
334
+ // Create standalone server
335
+ const server = FabricHttpServer({
336
+ services: [
337
+ userService,
338
+ productService,
339
+ { service: userService, path: "/users/:id", methods: ["GET", "PUT"] },
340
+ ],
341
+ prefix: "/api", // Optional path prefix
342
+ cors: true, // Server-level CORS (default: true)
343
+ });
344
+
345
+ // Export as Lambda handler
346
+ export const handler = lambdaHandler(server);
347
+ // Routes: /api/users, /api/products, /api/users/:id
348
+ ```
349
+
350
+ FabricHttpServer handles:
351
+ - API Gateway v1 (REST API) and v2 (HTTP API) event formats
352
+ - Route matching by path pattern and HTTP method
353
+ - CORS preflight requests and response headers
354
+ - JSON:API formatted responses (`{ data }` / `{ errors }`)
355
+ - 404 Not Found and 405 Method Not Allowed responses
356
+
357
+ ### Data Adapter (FabricData)
358
+
359
+ Generate CRUD HTTP services for Jaypie models backed by DynamoDB:
360
+
361
+ ```typescript
362
+ import { FabricData } from "@jaypie/fabric/data";
363
+ import { FabricHttpServer } from "@jaypie/fabric/http";
364
+
365
+ // Basic usage - creates all CRUD services
366
+ const recordServices = FabricData({ model: "record" });
367
+
368
+ // Use with FabricHttpServer
369
+ const server = FabricHttpServer({
370
+ services: recordServices.services,
371
+ prefix: "/api",
372
+ });
373
+
374
+ export const handler = server.handler;
375
+ // Routes: POST /api/records, GET /api/records, GET /api/records/:id,
376
+ // POST /api/records/:id, DELETE /api/records/:id, POST /api/records/:id/archive
377
+ ```
378
+
379
+ #### Route Mapping
380
+
381
+ | Operation | HTTP Method | Route | DynamoDB Function |
382
+ |-----------|-------------|-------|-------------------|
383
+ | create | POST | `/{model}` | `putEntity` |
384
+ | list | GET | `/{model}` | `queryByScope` |
385
+ | read | GET | `/{model}/:id` | `getEntity` |
386
+ | update | POST | `/{model}/:id` | `updateEntity` |
387
+ | delete | DELETE | `/{model}/:id` | `deleteEntity` |
388
+ | archive | POST | `/{model}/:id/archive` | `archiveEntity` |
389
+ | *custom* | POST | `/{model}/:id/{action}` | custom service |
390
+
391
+ Custom operations are defined in the `execute` array and create routes like `/records/:id/publish`.
392
+
393
+ #### Configuration
394
+
395
+ ```typescript
396
+ const services = FabricData({
397
+ // Model: string or config object
398
+ model: "record", // Or: { alias: "record", name: "Record", description: "..." }
399
+
400
+ // Authorization for all operations
401
+ authorization: async (token) => {
402
+ const user = await validateJwt(token);
403
+ if (!user) throw new UnauthorizedError();
404
+ return user;
405
+ },
406
+
407
+ // CORS configuration
408
+ cors: { origin: "*" },
409
+
410
+ // Scope calculator (default: APEX "@")
411
+ // Determines how entities are grouped for queries
412
+ scope: ({ params }) => `chat#${params.chatId}`,
413
+
414
+ // Pagination limits
415
+ defaultLimit: 20, // Default items per page
416
+ maxLimit: 100, // Maximum items per page
417
+
418
+ // Per-operation configuration
419
+ operations: {
420
+ read: { authorization: false }, // Public read
421
+ list: { authorization: false }, // Public list
422
+ delete: { authorization: requireAdmin }, // Admin-only delete
423
+ archive: false, // Disable archive
424
+ create: {
425
+ // Transform input before saving
426
+ transform: (input, existing) => ({
427
+ ...input,
428
+ createdBy: input.userId,
429
+ }),
430
+ },
431
+ },
432
+ });
433
+ ```
434
+
435
+ #### Custom Execute Actions
436
+
437
+ Add custom actions that operate on entities:
438
+
439
+ ```typescript
440
+ const services = FabricData({
441
+ model: "record",
442
+ execute: [
443
+ {
444
+ alias: "publish",
445
+ description: "Publish a record",
446
+ authorization: requireEditor,
447
+ input: {
448
+ publishDate: { type: Date, required: false },
449
+ notify: { type: Boolean, default: false },
450
+ },
451
+ service: async (entity, { publishDate, notify }) => {
452
+ // entity is the fetched record
453
+ const { updateEntity } = await import("@jaypie/dynamodb");
454
+ await updateEntity({
455
+ entity: {
456
+ ...entity,
457
+ metadata: { ...entity.metadata, publishedAt: publishDate ?? new Date() },
458
+ },
459
+ });
460
+ if (notify) await sendNotification(entity);
461
+ return { published: true };
462
+ },
463
+ },
464
+ {
465
+ alias: "duplicate",
466
+ description: "Create a copy of a record",
467
+ service: async (entity) => {
468
+ const { putEntity } = await import("@jaypie/dynamodb");
469
+ const duplicate = {
470
+ ...entity,
471
+ id: crypto.randomUUID(),
472
+ name: `${entity.name} (Copy)`,
473
+ };
474
+ delete duplicate.alias;
475
+ return putEntity({ entity: duplicate });
476
+ },
477
+ },
478
+ ],
479
+ });
480
+ // Routes: POST /records/:id/publish, POST /records/:id/duplicate
481
+ ```
482
+
483
+ #### List Pagination
484
+
485
+ The list operation supports pagination via cursor:
486
+
487
+ ```typescript
488
+ // First request
489
+ GET /api/records?limit=10
490
+
491
+ // Response
492
+ {
493
+ "data": {
494
+ "items": [...],
495
+ "nextKey": "eyJpZCI6Ii4uLiJ9" // Base64 encoded cursor
496
+ }
497
+ }
498
+
499
+ // Next page
500
+ GET /api/records?limit=10&cursor=eyJpZCI6Ii4uLiJ9
501
+ ```
502
+
503
+ Query parameters:
504
+ - `limit` - Items per page (default: 20, max: 100)
505
+ - `cursor` - Pagination cursor from previous response
506
+ - `ascending` - Sort ascending by sequence (default: false)
507
+ - `archived` - Include archived entities (default: false)
508
+ - `deleted` - Include deleted entities (default: false)
509
+
510
+ ### Modeling
511
+
512
+ FabricModel provides a standard vocabulary for entities. All fields are optional except `id` and `model`, enabling high reuse across different entity types.
513
+
514
+ ```typescript
515
+ import type { FabricModel } from "@jaypie/fabric";
516
+
517
+ const record: FabricModel = {
518
+ // Identity (required)
519
+ id: "550e8400-e29b-41d4-a716-446655440000",
520
+ model: "record",
521
+
522
+ // Identity (optional)
523
+ name: "December 12, 2026 Session", // Full name, first reference
524
+ label: "December 12", // Short name, second reference
525
+ abbreviation: "12/12", // Shortest form
526
+ alias: "2026-12-12", // Slug for human lookup
527
+ xid: "external-system-id", // External identifier
528
+ description: "Daily session notes",
529
+
530
+ // Schema
531
+ class: "memory", // Category (varies by model)
532
+ type: "session", // Type (varies by model)
533
+
534
+ // Content
535
+ content: "Session notes here...",
536
+ metadata: { tags: ["work", "planning"] },
537
+
538
+ // Display
539
+ emoji: "📝",
540
+ icon: "lucide#notebook",
541
+
542
+ // Timestamps
543
+ createdAt: new Date(),
544
+ updatedAt: new Date(),
545
+ archivedAt: null, // Set when archived
546
+ deletedAt: null, // Set when soft-deleted
547
+ };
548
+ ```
549
+
550
+ #### Specialized Models
551
+
552
+ **FabricJob** extends FabricModel for async tasks:
553
+
554
+ ```typescript
555
+ import type { FabricJob } from "@jaypie/fabric";
556
+
557
+ const job: FabricJob = {
558
+ id: "job-123",
559
+ model: "job",
560
+ status: "processing", // Required: current state
561
+ startedAt: new Date(),
562
+ completedAt: null,
563
+ progress: { // FabricProgress (value object)
564
+ percentageComplete: 45,
565
+ elapsedTime: 12000,
566
+ estimatedTime: 30000,
567
+ },
568
+ messages: [], // Execution log
569
+ createdAt: new Date(),
570
+ updatedAt: new Date(),
571
+ };
572
+ ```
573
+
574
+ **FabricMessage** extends FabricModel for content-focused entities:
575
+
576
+ ```typescript
577
+ import type { FabricMessage } from "@jaypie/fabric";
578
+
579
+ const message: FabricMessage = {
580
+ id: "msg-456",
581
+ model: "message",
582
+ content: "Hello, world!", // Required
583
+ type: "user", // e.g., "user", "assistant", "system"
584
+ createdAt: new Date(),
585
+ updatedAt: new Date(),
586
+ };
587
+ ```
588
+
589
+ #### Indexing
590
+
591
+ When persisting models to DynamoDB, use index utilities to build GSI keys:
592
+
593
+ ```typescript
594
+ import { APEX, calculateScope, populateIndexKeys, DEFAULT_INDEXES } from "@jaypie/fabric";
595
+
596
+ // Root-level entity
597
+ const record = {
598
+ model: "record",
599
+ scope: APEX, // "@" for root level
600
+ alias: "2026-12-12",
601
+ sequence: Date.now(),
602
+ // ...other fields
603
+ };
604
+
605
+ // Child entity (belongs to a parent)
606
+ const message = {
607
+ model: "message",
608
+ scope: calculateScope({ model: "chat", id: "chat-123" }), // "chat#chat-123"
609
+ sequence: Date.now(),
610
+ // ...other fields
611
+ };
612
+
613
+ // Auto-populate GSI keys
614
+ const indexed = populateIndexKeys(record, DEFAULT_INDEXES);
615
+ // indexed.indexScope = "@#record"
616
+ // indexed.indexAlias = "@#record#2026-12-12"
617
+ ```
618
+
619
+ ## API
620
+
621
+ ### Main Export (`@jaypie/fabric`)
622
+
623
+ | Export | Description |
624
+ |--------|-------------|
625
+ | `fabricService` | Factory function for validated service endpoints |
626
+ | `fabric` | Master conversion dispatcher |
627
+ | `fabricBoolean` | Convert to boolean |
628
+ | `fabricNumber` | Convert to number |
629
+ | `fabricString` | Convert to string |
630
+ | `fabricArray` | Wrap in array |
631
+ | `resolveFromArray` | Extract from single-element array |
632
+ | `fabricObject` | Wrap in `{ value: ... }` |
633
+ | `resolveFromObject` | Extract `.value` from object |
634
+ | `fabricDate` | Convert to Date |
635
+ | `resolveFromDate` | Resolve from Date to string |
636
+ | `FabricModel` | Base type for models |
637
+ | `FabricMessage` | Message model type |
638
+ | `FabricJob` | Job model type |
639
+ | `FabricProgress` | Progress tracking type |
640
+ | `registerModel` | Register custom indexes for a model |
641
+ | `getModelIndexes` | Get indexes for a model |
642
+ | `populateIndexKeys` | Populate GSI keys on an entity |
643
+ | `buildCompositeKey` | Build composite key from fields |
644
+ | `calculateScope` | Calculate scope |
645
+ | `DEFAULT_INDEXES` | Default GSI indexes |
646
+ | `APEX` | Root-level marker (`"@"`) |
647
+ | `SEPARATOR` | Composite key separator (`"#"`) |
648
+ | `ARCHIVED_SUFFIX` | Suffix for archived entities |
649
+ | `DELETED_SUFFIX` | Suffix for deleted entities |
650
+
651
+ ### Sub-Exports
652
+
653
+ | Path | Description |
654
+ |------|-------------|
655
+ | `@jaypie/fabric/commander` | Commander.js CLI adapter |
656
+ | `@jaypie/fabric/data` | DynamoDB CRUD service generator |
657
+ | `@jaypie/fabric/express` | Express middleware adapter |
658
+ | `@jaypie/fabric/http` | HTTP adapter with authorization and CORS |
659
+ | `@jaypie/fabric/lambda` | AWS Lambda adapter |
660
+ | `@jaypie/fabric/llm` | LLM tool adapter |
661
+ | `@jaypie/fabric/mcp` | MCP server adapter |
662
+
663
+ ## Philosophy
664
+
665
+ The "Fabric" philosophy:
666
+ - **Smooth, pliable** - Things that feel right should work
667
+ - **Catch bad passes** - Invalid inputs throw clear errors
668
+
669
+ This means:
670
+ - `"true"` works where `true` is expected
671
+ - `"42"` works where `42` is expected
672
+ - JSON strings automatically parse
673
+ - Invalid conversions fail fast with `BadRequestError`
674
+
675
+ ## License
676
+
677
+ MIT