@molecule/api-resource 1.0.0 → 1.0.1
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.
- package/README.md +628 -0
- package/package.json +12 -11
package/README.md
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
3
|
+
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
|
+
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
|
+
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
+
Generated: 2026-08-04T01:49:07.015Z
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# @molecule/api-resource
|
|
10
|
+
|
|
11
|
+
> **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
|
|
12
|
+
> It is written to be read by coding agents as much as by people, and is generated from this
|
|
13
|
+
> package's source — edit `src/index.ts` JSDoc, not this file.
|
|
14
|
+
|
|
15
|
+
Base resource patterns for molecule.dev.
|
|
16
|
+
|
|
17
|
+
Provides CRUD operation factories and request handler utilities for
|
|
18
|
+
building RESTful API resources. Every `@molecule/api-resource-*` package
|
|
19
|
+
(user, project, device, ...) is built on these factories.
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { z } from 'zod'
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
createRequestHandler,
|
|
28
|
+
create,
|
|
29
|
+
read,
|
|
30
|
+
update,
|
|
31
|
+
del,
|
|
32
|
+
query,
|
|
33
|
+
type MoleculeRequest,
|
|
34
|
+
} from '@molecule/api-resource'
|
|
35
|
+
import { types } from '@molecule/api-resource'
|
|
36
|
+
import { basePropsSchema } from '@molecule/api-resource/schema'
|
|
37
|
+
|
|
38
|
+
// Every resource schema extends basePropsSchema (id/createdAt/updatedAt).
|
|
39
|
+
const userSchema = basePropsSchema.extend({ name: z.string(), email: z.string() })
|
|
40
|
+
type UserProps = z.infer<typeof userSchema>
|
|
41
|
+
|
|
42
|
+
// Each CRUD factory takes the resource descriptor and returns an async
|
|
43
|
+
// operation, e.g. ({ props }) or ({ id }) => { statusCode, body }
|
|
44
|
+
// One descriptor, typed as Resource, drives every CRUD factory:
|
|
45
|
+
const userResource: types.Resource = { name: 'User', tableName: 'users', schema: userSchema }
|
|
46
|
+
|
|
47
|
+
const createUser = create<UserProps>(userResource)
|
|
48
|
+
const updateUser = update<UserProps>(userResource)
|
|
49
|
+
// read/del type their descriptor with the concrete props — cast the shared one:
|
|
50
|
+
const readUser = read(userResource as types.Resource<UserProps>)
|
|
51
|
+
const deleteUser = del(userResource as types.Resource<UserProps>)
|
|
52
|
+
|
|
53
|
+
// Operations do NOT take (req, res) directly — adapt each one by mapping
|
|
54
|
+
// request fields (req.body, req.params.id) to its arguments, then wrap
|
|
55
|
+
// with createRequestHandler for Express
|
|
56
|
+
const requestHandlerMap = {
|
|
57
|
+
create: createRequestHandler(
|
|
58
|
+
async (req: MoleculeRequest) => await createUser({ props: req.body }),
|
|
59
|
+
),
|
|
60
|
+
read: createRequestHandler(async (req: MoleculeRequest) => await readUser({ id: req.params.id })),
|
|
61
|
+
// ...
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Type
|
|
66
|
+
|
|
67
|
+
`infrastructure`
|
|
68
|
+
|
|
69
|
+
## Installation
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm install @molecule/api-resource @molecule/api-bond @molecule/api-database @molecule/api-i18n @molecule/api-locales-resource @molecule/api-utilities-validation uuid zod
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
### Interfaces
|
|
78
|
+
|
|
79
|
+
#### `ErrorFallback`
|
|
80
|
+
|
|
81
|
+
Fallback HTTP response used when a caught error is NOT a molecule-tagged error
|
|
82
|
+
(i.e. a genuinely-unexpected failure).
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
interface ErrorFallback {
|
|
86
|
+
/** HTTP status for the fallback (typically 500). */
|
|
87
|
+
status: number
|
|
88
|
+
/** Human-readable fallback message (already localized by the caller). */
|
|
89
|
+
message: string
|
|
90
|
+
/** Machine-readable key the frontend maps to a localized message. */
|
|
91
|
+
errorKey: string
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### `MoleculeRequest`
|
|
96
|
+
|
|
97
|
+
Framework-agnostic HTTP request interface. Structurally compatible with Express's
|
|
98
|
+
`Request` — Express objects satisfy this interface without adapters, but the type
|
|
99
|
+
doesn't depend on Express, allowing use with Fastify, Hono, or Lambda.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
interface MoleculeRequest {
|
|
103
|
+
body: any
|
|
104
|
+
params: Record<string, string>
|
|
105
|
+
query: Record<string, any>
|
|
106
|
+
headers: Record<string, string | string[] | undefined>
|
|
107
|
+
cookies?: Record<string, string>
|
|
108
|
+
/**
|
|
109
|
+
* Client IP address. Populated by Express (honoring `trust proxy`) and other
|
|
110
|
+
* frameworks; optional because it isn't guaranteed in every adapter. Used for
|
|
111
|
+
* IP-keyed rate limiting on auth endpoints.
|
|
112
|
+
*/
|
|
113
|
+
ip?: string
|
|
114
|
+
on?(event: string, listener: (...args: unknown[]) => void): void
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### `MoleculeResponse`
|
|
119
|
+
|
|
120
|
+
Framework-agnostic HTTP response interface. Structurally compatible with Express's
|
|
121
|
+
`Response` — includes `status()`, `json()`, `send()`, `set()`, cookie methods,
|
|
122
|
+
streaming (`write`), and `locals` for per-request data sharing.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
interface MoleculeResponse {
|
|
126
|
+
status(code: number): this
|
|
127
|
+
json(body: unknown): void
|
|
128
|
+
send(body: unknown): void
|
|
129
|
+
end(): void
|
|
130
|
+
set(headers: Record<string, string>): void
|
|
131
|
+
setHeader(name: string, value: string): void
|
|
132
|
+
cookie(name: string, value: string, options?: Record<string, unknown>): void
|
|
133
|
+
write(chunk: string | Buffer): boolean
|
|
134
|
+
flushHeaders?(): void
|
|
135
|
+
locals: Record<string, any>
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### `Query`
|
|
140
|
+
|
|
141
|
+
The available resource query options.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
interface Query {
|
|
145
|
+
/**
|
|
146
|
+
* The maximum number of query results.
|
|
147
|
+
*/
|
|
148
|
+
limit?: number
|
|
149
|
+
/**
|
|
150
|
+
* An indexed property to order the results by.
|
|
151
|
+
*/
|
|
152
|
+
orderBy?: `createdAt` | `updatedAt`
|
|
153
|
+
/**
|
|
154
|
+
* The direction to order the results.
|
|
155
|
+
*/
|
|
156
|
+
orderDirection?: `asc` | `desc`
|
|
157
|
+
/**
|
|
158
|
+
* Constrains the results to values less than these properties, when provided.
|
|
159
|
+
*/
|
|
160
|
+
before?: {
|
|
161
|
+
/**
|
|
162
|
+
* Constrains the results to resources created before the provided timestamp.
|
|
163
|
+
*
|
|
164
|
+
* Usually an ISO 8601 timestamp.
|
|
165
|
+
*/
|
|
166
|
+
createdAt?: string
|
|
167
|
+
/**
|
|
168
|
+
* Constrains the results to resources updated before the provided timestamp.
|
|
169
|
+
*
|
|
170
|
+
* Usually an ISO 8601 timestamp.
|
|
171
|
+
*/
|
|
172
|
+
updatedAt?: string
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Constrains the results to values greater than these properties, when provided.
|
|
176
|
+
*/
|
|
177
|
+
after?: {
|
|
178
|
+
/**
|
|
179
|
+
* Constrains the results to resources created after the provided timestamp.
|
|
180
|
+
*
|
|
181
|
+
* Usually an ISO 8601 timestamp.
|
|
182
|
+
*/
|
|
183
|
+
createdAt?: string
|
|
184
|
+
/**
|
|
185
|
+
* Constrains the results to resources updated after the provided timestamp.
|
|
186
|
+
*
|
|
187
|
+
* Usually an ISO 8601 timestamp.
|
|
188
|
+
*/
|
|
189
|
+
updatedAt?: string
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
#### `Response`
|
|
195
|
+
|
|
196
|
+
Structured response object returned by resource handlers. Contains the HTTP
|
|
197
|
+
status code, optional headers, and the response body payload.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
interface Response {
|
|
201
|
+
/**
|
|
202
|
+
* HTTP status code to return.
|
|
203
|
+
*/
|
|
204
|
+
statusCode: number
|
|
205
|
+
/**
|
|
206
|
+
* Optional map of HTTP header keys to values.
|
|
207
|
+
*/
|
|
208
|
+
headers?: Record<string, string>
|
|
209
|
+
/**
|
|
210
|
+
* The response payload to return.
|
|
211
|
+
*/
|
|
212
|
+
body: any
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Types
|
|
217
|
+
|
|
218
|
+
#### `Handler`
|
|
219
|
+
|
|
220
|
+
A resource handler function that processes a request and returns a structured
|
|
221
|
+
response object. If it returns `null` or `undefined`, the request is passed
|
|
222
|
+
to the next middleware via `next()`.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
type Handler = (
|
|
226
|
+
req: MoleculeRequest,
|
|
227
|
+
res: MoleculeResponse,
|
|
228
|
+
) => Promise<Response | null | undefined> | Response | null | undefined
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
#### `MoleculeNextFunction`
|
|
232
|
+
|
|
233
|
+
Framework-agnostic next function. Call with no arguments to pass to the next
|
|
234
|
+
middleware, or with an error to trigger error-handling middleware.
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
type MoleculeNextFunction = (err?: unknown) => void
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
#### `MoleculeRequestHandler`
|
|
241
|
+
|
|
242
|
+
Framework-agnostic middleware function signature matching Express's
|
|
243
|
+
`(req, res, next) => void` pattern.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
type MoleculeRequestHandler = (
|
|
247
|
+
req: MoleculeRequest,
|
|
248
|
+
res: MoleculeResponse,
|
|
249
|
+
next: MoleculeNextFunction,
|
|
250
|
+
) => void | Promise<void>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Functions
|
|
254
|
+
|
|
255
|
+
#### `create(resource)`
|
|
256
|
+
|
|
257
|
+
Creates a handler to create a resource.
|
|
258
|
+
|
|
259
|
+
Uses the bonded DataStore from `@molecule/api-database` for database-agnostic operations.
|
|
260
|
+
|
|
261
|
+
Example usage:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import { create as resourceCreate } from '@molecule/api-resource'
|
|
265
|
+
import type * as types from '../../types.js'
|
|
266
|
+
|
|
267
|
+
export const create = ({ name, tableName, schema }: types.Resource) => {
|
|
268
|
+
const create = resourceCreate({ name, tableName, schema })
|
|
269
|
+
|
|
270
|
+
return async (req: MoleculeRequest) => {
|
|
271
|
+
const props = req.body
|
|
272
|
+
return await create({ props })
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
function create({
|
|
279
|
+
name,
|
|
280
|
+
tableName,
|
|
281
|
+
schema,
|
|
282
|
+
}: types.Resource<unknown>): ({
|
|
283
|
+
props: createProps,
|
|
284
|
+
id,
|
|
285
|
+
}: {
|
|
286
|
+
props: CreateProps
|
|
287
|
+
id?: CreatedProps['id']
|
|
288
|
+
}) => Promise<
|
|
289
|
+
| { statusCode: number; body: { error: string; errorKey: string; props?: undefined } }
|
|
290
|
+
| { statusCode: number; body: { props: CreatedProps; error?: undefined; errorKey?: undefined } }
|
|
291
|
+
>
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
- `resource` — The resource descriptor.
|
|
295
|
+
- `resource.name` — Human-readable resource name used in error messages (e.g. `'User'`).
|
|
296
|
+
- `resource.tableName` — Database table to insert the new row into.
|
|
297
|
+
- `resource.schema` — Zod schema to validate the incoming props against.
|
|
298
|
+
|
|
299
|
+
**Returns:** A curried async function that accepts `{ props, id? }` and returns a `{ statusCode, body }` response.
|
|
300
|
+
|
|
301
|
+
#### `createRequestHandler(handler)`
|
|
302
|
+
|
|
303
|
+
Wraps a resource handler into Express-compatible middleware. Calls the handler,
|
|
304
|
+
then applies `statusCode`, `headers`, and `body` to the response. If the handler
|
|
305
|
+
returns null/undefined, calls `next()`. Catches errors and forwards them as
|
|
306
|
+
i18n-translated error messages.
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
function createRequestHandler(
|
|
310
|
+
handler: Handler,
|
|
311
|
+
): (req: MoleculeRequest, res: MoleculeResponse, next: MoleculeNextFunction) => Promise<void>
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
- `handler` — The resource handler function to wrap.
|
|
315
|
+
|
|
316
|
+
**Returns:** An Express-compatible middleware function `(req, res, next) => void`.
|
|
317
|
+
|
|
318
|
+
#### `del(resource)`
|
|
319
|
+
|
|
320
|
+
Creates a handler to delete a resource by its `id`.
|
|
321
|
+
|
|
322
|
+
> **Note:** We use `del` everywhere instead of `delete` since `delete` is a reserved keyword in JavaScript.
|
|
323
|
+
|
|
324
|
+
Uses the bonded DataStore from `@molecule/api-database` for database-agnostic operations.
|
|
325
|
+
|
|
326
|
+
Example usage:
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
import { del as resourceDel } from '@molecule/api-resource'
|
|
330
|
+
import type * as types from '../../types.js'
|
|
331
|
+
|
|
332
|
+
export const del = ({ name, tableName, schema }: types.Resource) => {
|
|
333
|
+
const del = resourceDel({ name, tableName, schema })
|
|
334
|
+
|
|
335
|
+
return async (req: MoleculeRequest) => {
|
|
336
|
+
const id = req.params.id
|
|
337
|
+
return await del({ id })
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
function del({ name, tableName }: DeletedResource): ({ id }: { id: Props['id'] }) => Promise<
|
|
344
|
+
| { statusCode: number; body: { error: string; errorKey: string; props?: undefined } }
|
|
345
|
+
| {
|
|
346
|
+
statusCode: number
|
|
347
|
+
body: { props: { id: Props['id'] }; error?: undefined; errorKey?: undefined }
|
|
348
|
+
}
|
|
349
|
+
>
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
- `resource` — The resource descriptor.
|
|
353
|
+
- `resource.name` — Human-readable resource name used in error messages (e.g. `'User'`).
|
|
354
|
+
- `resource.tableName` — Database table to delete the row from.
|
|
355
|
+
|
|
356
|
+
**Returns:** A curried async function that accepts `{ id }` and returns a `{ statusCode, body }` response.
|
|
357
|
+
|
|
358
|
+
#### `query(resource)`
|
|
359
|
+
|
|
360
|
+
Creates a request handler to be used to query resources.
|
|
361
|
+
|
|
362
|
+
Uses the bonded DataStore from `@molecule/api-database` for database-agnostic operations.
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
function query({
|
|
366
|
+
tableName,
|
|
367
|
+
}: QueriedResource): (
|
|
368
|
+
req: MoleculeRequest,
|
|
369
|
+
) => Promise<
|
|
370
|
+
| { statusCode: number; body: { error: string; errorKey: string } }
|
|
371
|
+
| { statusCode: number; body: Props[] }
|
|
372
|
+
>
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
- `resource` — The resource descriptor.
|
|
376
|
+
- `resource.tableName` — Database table to query resources from.
|
|
377
|
+
|
|
378
|
+
**Returns:** A curried async handler that accepts a `MoleculeRequest` (with query params for pagination/sorting) and returns a `{ statusCode, body }` response.
|
|
379
|
+
|
|
380
|
+
#### `read(resource)`
|
|
381
|
+
|
|
382
|
+
Creates a handler to read a resource by its `id`.
|
|
383
|
+
|
|
384
|
+
Uses the bonded DataStore from `@molecule/api-database` for database-agnostic operations.
|
|
385
|
+
|
|
386
|
+
Example usage:
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
import { read as resourceRead } from '@molecule/api-resource'
|
|
390
|
+
import type * as types from '../../types.js'
|
|
391
|
+
|
|
392
|
+
export const read = ({ name, tableName, schema }: types.Resource) => {
|
|
393
|
+
const read = resourceRead({ name, tableName, schema })
|
|
394
|
+
|
|
395
|
+
return async (req: MoleculeRequest) => {
|
|
396
|
+
const id = req.params.id
|
|
397
|
+
return await read({ id })
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
function read({
|
|
404
|
+
tableName,
|
|
405
|
+
}: ReadResource): ({
|
|
406
|
+
id,
|
|
407
|
+
props,
|
|
408
|
+
}: {
|
|
409
|
+
id: ReadProps['id']
|
|
410
|
+
props?: ReadProps
|
|
411
|
+
}) => Promise<
|
|
412
|
+
| { statusCode: number; body: { props: ReadProps; error?: undefined; errorKey?: undefined } }
|
|
413
|
+
| { statusCode: number; body: { error: string; errorKey: string; props?: undefined } }
|
|
414
|
+
>
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
- `resource` — The resource descriptor.
|
|
418
|
+
- `resource.tableName` — Database table to query the resource from.
|
|
419
|
+
|
|
420
|
+
**Returns:** A curried async function that accepts `{ id, props? }` and returns a `{ statusCode, body }` response.
|
|
421
|
+
|
|
422
|
+
#### `respondError(res, error, fallback)`
|
|
423
|
+
|
|
424
|
+
Respond to a caught handler error, honoring molecule's "tagged error" convention.
|
|
425
|
+
|
|
426
|
+
A tagged error carries BOTH a numeric `statusCode` AND a string `errorKey` — the same
|
|
427
|
+
contract `@molecule/api-server-default-express`'s error middleware (`classifyTaggedError`)
|
|
428
|
+
uses. Providers throw these for expected, user-actionable conditions — e.g. a
|
|
429
|
+
config-missing throw (`503` + `'config.notConfigured'`) when `STRIPE_SECRET_KEY` is unset.
|
|
430
|
+
|
|
431
|
+
Handlers that catch errors MUST funnel through this instead of a blanket
|
|
432
|
+
`res.status(500)`, so a tagged error surfaces its REAL status + errorKey rather than
|
|
433
|
+
being flattened to a generic 500. Otherwise the app looks broken at the exact moment the
|
|
434
|
+
user tries to use the feature — the worst possible time for a payment (the conversion
|
|
435
|
+
path). A swallowed tag is why the middleware-only fix was insufficient: ~185 handlers
|
|
436
|
+
catch-and-500, so the tag never reached the middleware.
|
|
437
|
+
|
|
438
|
+
Tagged errors are logged at `warn` (expected/actionable, not a server fault); everything
|
|
439
|
+
else is logged at `error` and returns the caller's `fallback`. Requiring BOTH fields is
|
|
440
|
+
deliberate: it keeps arbitrary library errors that merely carry a `.statusCode` (e.g. an
|
|
441
|
+
AWS SDK 403) from being surfaced with a status molecule never chose.
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
function respondError(res: MoleculeResponse, error: unknown, fallback: ErrorFallback): void
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
- `res` — The response object.
|
|
448
|
+
- `error` — The caught error.
|
|
449
|
+
- `fallback` — Status/message/errorKey to use when `error` isn't a tagged error.
|
|
450
|
+
|
|
451
|
+
#### `update(resource)`
|
|
452
|
+
|
|
453
|
+
Creates a handler to update a resource by its `id` and some `props`.
|
|
454
|
+
|
|
455
|
+
Uses the bonded DataStore from `@molecule/api-database` for database-agnostic operations.
|
|
456
|
+
|
|
457
|
+
Example usage:
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
import { update as resourceUpdate } from '@molecule/api-resource'
|
|
461
|
+
import type * as types from '../../types.js'
|
|
462
|
+
|
|
463
|
+
export const update = ({ name, tableName, schema }: types.Resource) => {
|
|
464
|
+
const update = resourceUpdate({ name, tableName, schema })
|
|
465
|
+
|
|
466
|
+
return async (req: MoleculeRequest) => {
|
|
467
|
+
const id = req.params.id
|
|
468
|
+
const props = req.body
|
|
469
|
+
return await update({ id, props })
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
function update({
|
|
476
|
+
name,
|
|
477
|
+
tableName,
|
|
478
|
+
schema,
|
|
479
|
+
}: types.Resource<unknown>): ({
|
|
480
|
+
id,
|
|
481
|
+
props: updateProps,
|
|
482
|
+
}: {
|
|
483
|
+
id: UpdatedProps['id'] | types.Props['id']
|
|
484
|
+
props: UpdateProps
|
|
485
|
+
}) => Promise<
|
|
486
|
+
| { statusCode: number; body: { error: string; errorKey: string; props?: undefined } }
|
|
487
|
+
| { statusCode: number; body: { props: UpdatedProps; error?: undefined; errorKey?: undefined } }
|
|
488
|
+
>
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
- `resource` — The resource descriptor.
|
|
492
|
+
- `resource.name` — Human-readable resource name used in error messages (e.g. `'User'`).
|
|
493
|
+
- `resource.tableName` — Database table to update the row in.
|
|
494
|
+
- `resource.schema` — Zod schema to validate the incoming props against.
|
|
495
|
+
|
|
496
|
+
**Returns:** A curried async function that accepts `{ id, props }` and returns a `{ statusCode, body }` response.
|
|
497
|
+
|
|
498
|
+
### Constants
|
|
499
|
+
|
|
500
|
+
#### `i18nRegistered`
|
|
501
|
+
|
|
502
|
+
Sentinel value confirming that locale translations for the resource package
|
|
503
|
+
have been registered (or attempted). Always `true` after this module loads.
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
const i18nRegistered: true
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
#### `querySchema`
|
|
510
|
+
|
|
511
|
+
The resource query Zod schema.
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
const querySchema: z.ZodObject<
|
|
515
|
+
{
|
|
516
|
+
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>
|
|
517
|
+
orderBy: z.ZodDefault<z.ZodEnum<{ createdAt: 'createdAt'; updatedAt: 'updatedAt' }>>
|
|
518
|
+
orderDirection: z.ZodDefault<z.ZodEnum<{ asc: 'asc'; desc: 'desc' }>>
|
|
519
|
+
before: z.ZodOptional<
|
|
520
|
+
z.ZodObject<
|
|
521
|
+
{ createdAt: z.ZodOptional<z.ZodString>; updatedAt: z.ZodOptional<z.ZodString> },
|
|
522
|
+
z.core.$strip
|
|
523
|
+
>
|
|
524
|
+
>
|
|
525
|
+
after: z.ZodOptional<
|
|
526
|
+
z.ZodObject<
|
|
527
|
+
{ createdAt: z.ZodOptional<z.ZodString>; updatedAt: z.ZodOptional<z.ZodString> },
|
|
528
|
+
z.core.$strip
|
|
529
|
+
>
|
|
530
|
+
>
|
|
531
|
+
},
|
|
532
|
+
z.core.$strip
|
|
533
|
+
>
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### Namespaces
|
|
537
|
+
|
|
538
|
+
#### `schema`
|
|
539
|
+
|
|
540
|
+
Members:
|
|
541
|
+
|
|
542
|
+
- `schema.basePropsSchema` — const: Base Zod schema with the three fields every resource row must have:
|
|
543
|
+
- `schema.BaseProps` — type: TypeScript type inferred from `basePropsSchema`, containing `id`, `createdAt`, and `updatedAt`.
|
|
544
|
+
- `schema.responseSchema` — function: Creates a Zod schema for the standard `{ statusCode, body }` response
|
|
545
|
+
- `schema.resourceSchema` — function: Creates a Zod schema that validates a resource descriptor object
|
|
546
|
+
- `schema.validate` — function: Validates data against a Zod schema, throwing an `Error` with a
|
|
547
|
+
- `schema.safeParse` — function: Validates data against a Zod schema without throwing. Returns a discriminated
|
|
548
|
+
- `schema.partial` — function: Creates a schema where all fields are optional, suitable for validating
|
|
549
|
+
- `schema.pick` — function: Creates a schema with only the specified fields, useful for create payloads
|
|
550
|
+
|
|
551
|
+
#### `types`
|
|
552
|
+
|
|
553
|
+
Members:
|
|
554
|
+
|
|
555
|
+
- `types.BaseProps` — type: TypeScript type inferred from `basePropsSchema`, containing `id`, `createdAt`, and `updatedAt`.
|
|
556
|
+
- `types.Props` — interface: The resource's properties to be stored in the database.
|
|
557
|
+
- `types.Resource` — interface: An object describing the resource.
|
|
558
|
+
- `types.ZodResource` — interface: An object describing the resource with a Zod schema.
|
|
559
|
+
- `types.Response` — interface: Standard response from resource operations.
|
|
560
|
+
|
|
561
|
+
#### `utilities`
|
|
562
|
+
|
|
563
|
+
Members:
|
|
564
|
+
|
|
565
|
+
- `utilities.getValidProps` — const: Validates resource props against a Zod schema, returning typed valid props or throwing
|
|
566
|
+
- `utilities.safeParse` — const: Validates data against a Zod schema without throwing, returning a discriminated union.
|
|
567
|
+
- `utilities.validate` — const: Validates data against a Zod schema, throwing a ZodError on failure.
|
|
568
|
+
- `utilities.ZodError` — interface: An Error-like class used to store Zod validation issues.
|
|
569
|
+
- `utilities.ZodSchema` — interface
|
|
570
|
+
- `utilities.isEmail` — const: Basic regex for determining whether or not a string is an email address.
|
|
571
|
+
- `utilities.isUuid` — const: Basic regex for determining whether or not a string is a UUID.
|
|
572
|
+
|
|
573
|
+
## Injection Notes
|
|
574
|
+
|
|
575
|
+
### Requirements
|
|
576
|
+
|
|
577
|
+
Peer dependencies:
|
|
578
|
+
|
|
579
|
+
- `@molecule/api-bond` ^1.0.1
|
|
580
|
+
- `@molecule/api-database` ^1.0.1
|
|
581
|
+
- `@molecule/api-i18n` ^1.0.1
|
|
582
|
+
- `@molecule/api-locales-resource` ^1.0.1
|
|
583
|
+
- `@molecule/api-utilities-validation` ^1.0.1
|
|
584
|
+
|
|
585
|
+
### Runtime Dependencies
|
|
586
|
+
|
|
587
|
+
- `@molecule/api-bond`
|
|
588
|
+
- `@molecule/api-database`
|
|
589
|
+
- `@molecule/api-i18n`
|
|
590
|
+
- `@molecule/api-locales-resource`
|
|
591
|
+
- `@molecule/api-utilities-validation`
|
|
592
|
+
- `uuid`
|
|
593
|
+
- `zod`
|
|
594
|
+
|
|
595
|
+
Working WITH installed `@molecule/api-resource-*` packages (auth `getUserId`,
|
|
596
|
+
table ownership, migrations) — what generated code most often breaks:
|
|
597
|
+
|
|
598
|
+
- **Auth inside a handler is `const userId = getUserId(res)`, imported from
|
|
599
|
+
`@molecule/api-bonds-default-express`** — NOT from this package (this
|
|
600
|
+
package's `utilities` namespace holds only validation helpers). Return 401
|
|
601
|
+
when it is missing. There is NO `requireAuth` middleware or file to import
|
|
602
|
+
or create; auth is applied at the router level. A route's authorizer may
|
|
603
|
+
also stash the already-loaded row on the response context and hand it to
|
|
604
|
+
`read({ id, props })` to skip a second database fetch.
|
|
605
|
+
- **A selected `@molecule/api-resource-*` package OWNS its table AND its
|
|
606
|
+
already-wired routes with a FIXED schema** (e.g. `api-resource-project` owns
|
|
607
|
+
`projects` keyed by `user_id` with a live `/projects` handler). Do NOT write
|
|
608
|
+
a migration that re-creates such a table with a different schema — your
|
|
609
|
+
`CREATE TABLE` shadows theirs (or vice-versa) and the package's still-wired
|
|
610
|
+
handler then 500s (`column "user_id" does not exist`). To model a different
|
|
611
|
+
shape, use a DIFFERENT table name (e.g. `workspace_projects`) + your own
|
|
612
|
+
handler. Check `api/migrations/*` + `api/__setup__/` for tables packages
|
|
613
|
+
already provide.
|
|
614
|
+
- **Adding a resource package AFTER scaffold? Copy its migration.** Such a
|
|
615
|
+
package ships its table DDL at `node_modules/@molecule/<pkg>/src/__setup__/*.sql`
|
|
616
|
+
— copy it into `api/migrations/<next-number>_<name>.sql`. Wiring its
|
|
617
|
+
routes/handler alone does NOT create the table: every request 500s with
|
|
618
|
+
"relation does not exist" even though the build type-checks clean. (Only
|
|
619
|
+
packages present at scaffold time get their migration auto-created.)
|
|
620
|
+
- **Zod/validation helpers are NAMESPACED exports**, not top-level:
|
|
621
|
+
`schema.basePropsSchema` / `schema.validate` / `schema.safeParse` /
|
|
622
|
+
`schema.partial` / `schema.pick`, and `utilities.getValidProps`
|
|
623
|
+
(re-exported from `@molecule/api-utilities-validation`). Import the
|
|
624
|
+
namespace: `import { schema, utilities } from '@molecule/api-resource'`.
|
|
625
|
+
|
|
626
|
+
## Translations
|
|
627
|
+
|
|
628
|
+
Translation strings are provided by `@molecule/api-locales-resource`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-resource",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Resource CRUD operations and utilities for molecule.dev.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
-
"dist"
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md"
|
|
29
30
|
],
|
|
30
31
|
"keywords": [
|
|
31
32
|
"molecule",
|
|
@@ -38,20 +39,20 @@
|
|
|
38
39
|
"zod": "4.4.3"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
|
-
"@molecule/api-bond": "1.0.
|
|
42
|
-
"@molecule/api-database": "1.0.
|
|
43
|
-
"@molecule/api-locales-resource": "1.0.
|
|
44
|
-
"@molecule/api-utilities-validation": "1.0.
|
|
42
|
+
"@molecule/api-bond": "1.0.1",
|
|
43
|
+
"@molecule/api-database": "1.0.1",
|
|
44
|
+
"@molecule/api-locales-resource": "1.0.1",
|
|
45
|
+
"@molecule/api-utilities-validation": "1.0.1",
|
|
45
46
|
"@types/node": "26.1.2",
|
|
46
47
|
"typescript": "6.0.3",
|
|
47
48
|
"vitest": "4.1.10"
|
|
48
49
|
},
|
|
49
50
|
"peerDependencies": {
|
|
50
|
-
"@molecule/api-bond": "^1.0.
|
|
51
|
-
"@molecule/api-database": "^1.0.
|
|
52
|
-
"@molecule/api-i18n": "^1.0.
|
|
53
|
-
"@molecule/api-locales-resource": "^1.0.
|
|
54
|
-
"@molecule/api-utilities-validation": "^1.0.
|
|
51
|
+
"@molecule/api-bond": "^1.0.1",
|
|
52
|
+
"@molecule/api-database": "^1.0.1",
|
|
53
|
+
"@molecule/api-i18n": "^1.0.1",
|
|
54
|
+
"@molecule/api-locales-resource": "^1.0.1",
|
|
55
|
+
"@molecule/api-utilities-validation": "^1.0.1"
|
|
55
56
|
},
|
|
56
57
|
"repository": {
|
|
57
58
|
"type": "git",
|