@molecule/api-resource 1.0.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.
- package/LICENSE +115 -0
- package/dist/browser-guard.d.ts +2 -0
- package/dist/browser-guard.d.ts.map +1 -0
- package/dist/browser-guard.js +19 -0
- package/dist/browser-guard.js.map +1 -0
- package/dist/create.d.ts +58 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +87 -0
- package/dist/create.js.map +1 -0
- package/dist/createRequestHandler.d.ts +48 -0
- package/dist/createRequestHandler.d.ts.map +1 -0
- package/dist/createRequestHandler.js +42 -0
- package/dist/createRequestHandler.js.map +1 -0
- package/dist/del.d.ts +55 -0
- package/dist/del.d.ts.map +1 -0
- package/dist/del.js +63 -0
- package/dist/del.js.map +1 -0
- package/dist/http-types.d.ts +58 -0
- package/dist/http-types.d.ts.map +1 -0
- package/dist/http-types.js +11 -0
- package/dist/http-types.js.map +1 -0
- package/dist/i18n.d.ts +11 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +19 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +100 -0
- package/dist/index.js.map +1 -0
- package/dist/query.d.ts +102 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +100 -0
- package/dist/query.js.map +1 -0
- package/dist/read.d.ts +57 -0
- package/dist/read.d.ts.map +1 -0
- package/dist/read.js +56 -0
- package/dist/read.js.map +1 -0
- package/dist/respond-error.d.ts +44 -0
- package/dist/respond-error.d.ts.map +1 -0
- package/dist/respond-error.js +46 -0
- package/dist/respond-error.js.map +1 -0
- package/dist/schema.d.ts +81 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +112 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/dist/update.d.ts +59 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +82 -0
- package/dist/update.js.map +1 -0
- package/dist/utilities/index.d.ts +9 -0
- package/dist/utilities/index.d.ts.map +1 -0
- package/dist/utilities/index.js +9 -0
- package/dist/utilities/index.js.map +1 -0
- package/package.json +66 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base resource patterns for molecule.dev.
|
|
3
|
+
*
|
|
4
|
+
* Provides CRUD operation factories and request handler utilities for
|
|
5
|
+
* building RESTful API resources. Every `@molecule/api-resource-*` package
|
|
6
|
+
* (user, project, device, ...) is built on these factories.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { z } from 'zod'
|
|
11
|
+
*
|
|
12
|
+
* import {
|
|
13
|
+
* createRequestHandler,
|
|
14
|
+
* create,
|
|
15
|
+
* read,
|
|
16
|
+
* update,
|
|
17
|
+
* del,
|
|
18
|
+
* query,
|
|
19
|
+
* type MoleculeRequest,
|
|
20
|
+
* } from '@molecule/api-resource'
|
|
21
|
+
* import { types } from '@molecule/api-resource'
|
|
22
|
+
* import { basePropsSchema } from '@molecule/api-resource/schema'
|
|
23
|
+
*
|
|
24
|
+
* // Every resource schema extends basePropsSchema (id/createdAt/updatedAt).
|
|
25
|
+
* const userSchema = basePropsSchema.extend({ name: z.string(), email: z.string() })
|
|
26
|
+
* type UserProps = z.infer<typeof userSchema>
|
|
27
|
+
*
|
|
28
|
+
* // Each CRUD factory takes the resource descriptor and returns an async
|
|
29
|
+
* // operation, e.g. ({ props }) or ({ id }) => { statusCode, body }
|
|
30
|
+
* // One descriptor, typed as Resource, drives every CRUD factory:
|
|
31
|
+
* const userResource: types.Resource = { name: 'User', tableName: 'users', schema: userSchema }
|
|
32
|
+
*
|
|
33
|
+
* const createUser = create<UserProps>(userResource)
|
|
34
|
+
* const updateUser = update<UserProps>(userResource)
|
|
35
|
+
* // read/del type their descriptor with the concrete props — cast the shared one:
|
|
36
|
+
* const readUser = read(userResource as types.Resource<UserProps>)
|
|
37
|
+
* const deleteUser = del(userResource as types.Resource<UserProps>)
|
|
38
|
+
*
|
|
39
|
+
* // Operations do NOT take (req, res) directly — adapt each one by mapping
|
|
40
|
+
* // request fields (req.body, req.params.id) to its arguments, then wrap
|
|
41
|
+
* // with createRequestHandler for Express
|
|
42
|
+
* const requestHandlerMap = {
|
|
43
|
+
* create: createRequestHandler(async (req: MoleculeRequest) =>
|
|
44
|
+
* await createUser({ props: req.body }),
|
|
45
|
+
* ),
|
|
46
|
+
* read: createRequestHandler(async (req: MoleculeRequest) =>
|
|
47
|
+
* await readUser({ id: req.params.id }),
|
|
48
|
+
* ),
|
|
49
|
+
* // ...
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* Working WITH installed `@molecule/api-resource-*` packages (auth `getUserId`,
|
|
55
|
+
* table ownership, migrations) — what generated code most often breaks:
|
|
56
|
+
*
|
|
57
|
+
* - **Auth inside a handler is `const userId = getUserId(res)`, imported from
|
|
58
|
+
* `@molecule/api-bonds-default-express`** — NOT from this package (this
|
|
59
|
+
* package's `utilities` namespace holds only validation helpers). Return 401
|
|
60
|
+
* when it is missing. There is NO `requireAuth` middleware or file to import
|
|
61
|
+
* or create; auth is applied at the router level. A route's authorizer may
|
|
62
|
+
* also stash the already-loaded row on the response context and hand it to
|
|
63
|
+
* `read({ id, props })` to skip a second database fetch.
|
|
64
|
+
* - **A selected `@molecule/api-resource-*` package OWNS its table AND its
|
|
65
|
+
* already-wired routes with a FIXED schema** (e.g. `api-resource-project` owns
|
|
66
|
+
* `projects` keyed by `user_id` with a live `/projects` handler). Do NOT write
|
|
67
|
+
* a migration that re-creates such a table with a different schema — your
|
|
68
|
+
* `CREATE TABLE` shadows theirs (or vice-versa) and the package's still-wired
|
|
69
|
+
* handler then 500s (`column "user_id" does not exist`). To model a different
|
|
70
|
+
* shape, use a DIFFERENT table name (e.g. `workspace_projects`) + your own
|
|
71
|
+
* handler. Check `api/migrations/*` + `api/__setup__/` for tables packages
|
|
72
|
+
* already provide.
|
|
73
|
+
* - **Adding a resource package AFTER scaffold? Copy its migration.** Such a
|
|
74
|
+
* package ships its table DDL at `node_modules/@molecule/<pkg>/src/__setup__/*.sql`
|
|
75
|
+
* — copy it into `api/migrations/<next-number>_<name>.sql`. Wiring its
|
|
76
|
+
* routes/handler alone does NOT create the table: every request 500s with
|
|
77
|
+
* "relation does not exist" even though the build type-checks clean. (Only
|
|
78
|
+
* packages present at scaffold time get their migration auto-created.)
|
|
79
|
+
* - **Zod/validation helpers are NAMESPACED exports**, not top-level:
|
|
80
|
+
* `schema.basePropsSchema` / `schema.validate` / `schema.safeParse` /
|
|
81
|
+
* `schema.partial` / `schema.pick`, and `utilities.getValidProps`
|
|
82
|
+
* (re-exported from `@molecule/api-utilities-validation`). Import the
|
|
83
|
+
* namespace: `import { schema, utilities } from '@molecule/api-resource'`.
|
|
84
|
+
*
|
|
85
|
+
* @module
|
|
86
|
+
*/
|
|
87
|
+
export * from './browser-guard.js';
|
|
88
|
+
export * from './create.js';
|
|
89
|
+
export * from './createRequestHandler.js';
|
|
90
|
+
export * from './del.js';
|
|
91
|
+
export * from './http-types.js';
|
|
92
|
+
export * from './i18n.js';
|
|
93
|
+
export * from './query.js';
|
|
94
|
+
export * from './read.js';
|
|
95
|
+
export * from './respond-error.js';
|
|
96
|
+
export * as schema from './schema.js';
|
|
97
|
+
export * as types from './types.js';
|
|
98
|
+
export * from './update.js';
|
|
99
|
+
export * as utilities from './utilities/index.js';
|
|
100
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,2BAA2B,CAAA;AACzC,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,oBAAoB,CAAA;AAClC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AACnC,cAAc,aAAa,CAAA;AAC3B,OAAO,KAAK,SAAS,MAAM,sBAAsB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base resource patterns for molecule.dev.
|
|
3
|
+
*
|
|
4
|
+
* Provides CRUD operation factories and request handler utilities for
|
|
5
|
+
* building RESTful API resources. Every `@molecule/api-resource-*` package
|
|
6
|
+
* (user, project, device, ...) is built on these factories.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { z } from 'zod'
|
|
11
|
+
*
|
|
12
|
+
* import {
|
|
13
|
+
* createRequestHandler,
|
|
14
|
+
* create,
|
|
15
|
+
* read,
|
|
16
|
+
* update,
|
|
17
|
+
* del,
|
|
18
|
+
* query,
|
|
19
|
+
* type MoleculeRequest,
|
|
20
|
+
* } from '@molecule/api-resource'
|
|
21
|
+
* import { types } from '@molecule/api-resource'
|
|
22
|
+
* import { basePropsSchema } from '@molecule/api-resource/schema'
|
|
23
|
+
*
|
|
24
|
+
* // Every resource schema extends basePropsSchema (id/createdAt/updatedAt).
|
|
25
|
+
* const userSchema = basePropsSchema.extend({ name: z.string(), email: z.string() })
|
|
26
|
+
* type UserProps = z.infer<typeof userSchema>
|
|
27
|
+
*
|
|
28
|
+
* // Each CRUD factory takes the resource descriptor and returns an async
|
|
29
|
+
* // operation, e.g. ({ props }) or ({ id }) => { statusCode, body }
|
|
30
|
+
* // One descriptor, typed as Resource, drives every CRUD factory:
|
|
31
|
+
* const userResource: types.Resource = { name: 'User', tableName: 'users', schema: userSchema }
|
|
32
|
+
*
|
|
33
|
+
* const createUser = create<UserProps>(userResource)
|
|
34
|
+
* const updateUser = update<UserProps>(userResource)
|
|
35
|
+
* // read/del type their descriptor with the concrete props — cast the shared one:
|
|
36
|
+
* const readUser = read(userResource as types.Resource<UserProps>)
|
|
37
|
+
* const deleteUser = del(userResource as types.Resource<UserProps>)
|
|
38
|
+
*
|
|
39
|
+
* // Operations do NOT take (req, res) directly — adapt each one by mapping
|
|
40
|
+
* // request fields (req.body, req.params.id) to its arguments, then wrap
|
|
41
|
+
* // with createRequestHandler for Express
|
|
42
|
+
* const requestHandlerMap = {
|
|
43
|
+
* create: createRequestHandler(async (req: MoleculeRequest) =>
|
|
44
|
+
* await createUser({ props: req.body }),
|
|
45
|
+
* ),
|
|
46
|
+
* read: createRequestHandler(async (req: MoleculeRequest) =>
|
|
47
|
+
* await readUser({ id: req.params.id }),
|
|
48
|
+
* ),
|
|
49
|
+
* // ...
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* Working WITH installed `@molecule/api-resource-*` packages (auth `getUserId`,
|
|
55
|
+
* table ownership, migrations) — what generated code most often breaks:
|
|
56
|
+
*
|
|
57
|
+
* - **Auth inside a handler is `const userId = getUserId(res)`, imported from
|
|
58
|
+
* `@molecule/api-bonds-default-express`** — NOT from this package (this
|
|
59
|
+
* package's `utilities` namespace holds only validation helpers). Return 401
|
|
60
|
+
* when it is missing. There is NO `requireAuth` middleware or file to import
|
|
61
|
+
* or create; auth is applied at the router level. A route's authorizer may
|
|
62
|
+
* also stash the already-loaded row on the response context and hand it to
|
|
63
|
+
* `read({ id, props })` to skip a second database fetch.
|
|
64
|
+
* - **A selected `@molecule/api-resource-*` package OWNS its table AND its
|
|
65
|
+
* already-wired routes with a FIXED schema** (e.g. `api-resource-project` owns
|
|
66
|
+
* `projects` keyed by `user_id` with a live `/projects` handler). Do NOT write
|
|
67
|
+
* a migration that re-creates such a table with a different schema — your
|
|
68
|
+
* `CREATE TABLE` shadows theirs (or vice-versa) and the package's still-wired
|
|
69
|
+
* handler then 500s (`column "user_id" does not exist`). To model a different
|
|
70
|
+
* shape, use a DIFFERENT table name (e.g. `workspace_projects`) + your own
|
|
71
|
+
* handler. Check `api/migrations/*` + `api/__setup__/` for tables packages
|
|
72
|
+
* already provide.
|
|
73
|
+
* - **Adding a resource package AFTER scaffold? Copy its migration.** Such a
|
|
74
|
+
* package ships its table DDL at `node_modules/@molecule/<pkg>/src/__setup__/*.sql`
|
|
75
|
+
* — copy it into `api/migrations/<next-number>_<name>.sql`. Wiring its
|
|
76
|
+
* routes/handler alone does NOT create the table: every request 500s with
|
|
77
|
+
* "relation does not exist" even though the build type-checks clean. (Only
|
|
78
|
+
* packages present at scaffold time get their migration auto-created.)
|
|
79
|
+
* - **Zod/validation helpers are NAMESPACED exports**, not top-level:
|
|
80
|
+
* `schema.basePropsSchema` / `schema.validate` / `schema.safeParse` /
|
|
81
|
+
* `schema.partial` / `schema.pick`, and `utilities.getValidProps`
|
|
82
|
+
* (re-exported from `@molecule/api-utilities-validation`). Import the
|
|
83
|
+
* namespace: `import { schema, utilities } from '@molecule/api-resource'`.
|
|
84
|
+
*
|
|
85
|
+
* @module
|
|
86
|
+
*/
|
|
87
|
+
export * from './browser-guard.js';
|
|
88
|
+
export * from './create.js';
|
|
89
|
+
export * from './createRequestHandler.js';
|
|
90
|
+
export * from './del.js';
|
|
91
|
+
export * from './http-types.js';
|
|
92
|
+
export * from './i18n.js';
|
|
93
|
+
export * from './query.js';
|
|
94
|
+
export * from './read.js';
|
|
95
|
+
export * from './respond-error.js';
|
|
96
|
+
export * as schema from './schema.js';
|
|
97
|
+
export * as types from './types.js';
|
|
98
|
+
export * from './update.js';
|
|
99
|
+
export * as utilities from './utilities/index.js';
|
|
100
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,2BAA2B,CAAA;AACzC,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,oBAAoB,CAAA;AAClC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AACnC,cAAc,aAAa,CAAA;AAC3B,OAAO,KAAK,SAAS,MAAM,sBAAsB,CAAA"}
|
package/dist/query.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource query operation.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import type { MoleculeRequest } from './http-types.js';
|
|
8
|
+
import type * as types from './types.js';
|
|
9
|
+
/**
|
|
10
|
+
* The available resource query options.
|
|
11
|
+
*
|
|
12
|
+
* @example `/api/resources?limit=100&orderBy=updatedAt&after[updatedAt]=2022-02-22T02:22:22.222Z`
|
|
13
|
+
*/
|
|
14
|
+
export interface Query {
|
|
15
|
+
/**
|
|
16
|
+
* The maximum number of query results.
|
|
17
|
+
*/
|
|
18
|
+
limit?: number;
|
|
19
|
+
/**
|
|
20
|
+
* An indexed property to order the results by.
|
|
21
|
+
*/
|
|
22
|
+
orderBy?: `createdAt` | `updatedAt`;
|
|
23
|
+
/**
|
|
24
|
+
* The direction to order the results.
|
|
25
|
+
*/
|
|
26
|
+
orderDirection?: `asc` | `desc`;
|
|
27
|
+
/**
|
|
28
|
+
* Constrains the results to values less than these properties, when provided.
|
|
29
|
+
*/
|
|
30
|
+
before?: {
|
|
31
|
+
/**
|
|
32
|
+
* Constrains the results to resources created before the provided timestamp.
|
|
33
|
+
*
|
|
34
|
+
* Usually an ISO 8601 timestamp.
|
|
35
|
+
*/
|
|
36
|
+
createdAt?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Constrains the results to resources updated before the provided timestamp.
|
|
39
|
+
*
|
|
40
|
+
* Usually an ISO 8601 timestamp.
|
|
41
|
+
*/
|
|
42
|
+
updatedAt?: string;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Constrains the results to values greater than these properties, when provided.
|
|
46
|
+
*/
|
|
47
|
+
after?: {
|
|
48
|
+
/**
|
|
49
|
+
* Constrains the results to resources created after the provided timestamp.
|
|
50
|
+
*
|
|
51
|
+
* Usually an ISO 8601 timestamp.
|
|
52
|
+
*/
|
|
53
|
+
createdAt?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Constrains the results to resources updated after the provided timestamp.
|
|
56
|
+
*
|
|
57
|
+
* Usually an ISO 8601 timestamp.
|
|
58
|
+
*/
|
|
59
|
+
updatedAt?: string;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The resource query Zod schema.
|
|
64
|
+
*/
|
|
65
|
+
export declare const querySchema: z.ZodObject<{
|
|
66
|
+
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
67
|
+
orderBy: z.ZodDefault<z.ZodEnum<{
|
|
68
|
+
createdAt: "createdAt";
|
|
69
|
+
updatedAt: "updatedAt";
|
|
70
|
+
}>>;
|
|
71
|
+
orderDirection: z.ZodDefault<z.ZodEnum<{
|
|
72
|
+
asc: "asc";
|
|
73
|
+
desc: "desc";
|
|
74
|
+
}>>;
|
|
75
|
+
before: z.ZodOptional<z.ZodObject<{
|
|
76
|
+
createdAt: z.ZodOptional<z.ZodString>;
|
|
77
|
+
updatedAt: z.ZodOptional<z.ZodString>;
|
|
78
|
+
}, z.core.$strip>>;
|
|
79
|
+
after: z.ZodOptional<z.ZodObject<{
|
|
80
|
+
createdAt: z.ZodOptional<z.ZodString>;
|
|
81
|
+
updatedAt: z.ZodOptional<z.ZodString>;
|
|
82
|
+
}, z.core.$strip>>;
|
|
83
|
+
}, z.core.$strip>;
|
|
84
|
+
/**
|
|
85
|
+
* Creates a request handler to be used to query resources.
|
|
86
|
+
*
|
|
87
|
+
* Uses the bonded DataStore from `@molecule/api-database` for database-agnostic operations.
|
|
88
|
+
* @param resource - The resource descriptor.
|
|
89
|
+
* @param resource.tableName - Database table to query resources from.
|
|
90
|
+
* @returns A curried async handler that accepts a `MoleculeRequest` (with query params for pagination/sorting) and returns a `{ statusCode, body }` response.
|
|
91
|
+
*/
|
|
92
|
+
export declare const query: <Props extends types.Props = types.Props, QueriedResource extends types.Resource = types.Resource>({ tableName, }: QueriedResource) => (req: MoleculeRequest) => Promise<{
|
|
93
|
+
statusCode: number;
|
|
94
|
+
body: {
|
|
95
|
+
error: string;
|
|
96
|
+
errorKey: string;
|
|
97
|
+
};
|
|
98
|
+
} | {
|
|
99
|
+
statusCode: number;
|
|
100
|
+
body: Props[];
|
|
101
|
+
}>;
|
|
102
|
+
//# sourceMappingURL=query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAMtD,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAExC;;;;GAIG;AACH,MAAM,WAAW,KAAK;IACpB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;OAEG;IACH,OAAO,CAAC,EAAE,WAAW,GAAG,WAAW,CAAA;IACnC;;OAEG;IACH,cAAc,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;IAC/B;;OAEG;IACH,MAAM,CAAC,EAAE;QACP;;;;WAIG;QACH,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB;;;;WAIG;QACH,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB,CAAA;IACD;;OAEG;IACH,KAAK,CAAC,EAAE;QACN;;;;WAIG;QACH,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB;;;;WAIG;QACH,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB,CAAA;CACF;AAED;;GAEG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;iBAgBtB,CAAA;AAIF;;;;;;;GAOG;AACH,eAAO,MAAM,KAAK,GAEd,KAAK,SAAS,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,EACvC,eAAe,SAAS,KAAK,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,EACvD,gBAEC,eAAe,MACX,KAAK,eAAe;;;;;;;;;EAqE1B,CAAA"}
|
package/dist/query.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource query operation.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { getLogger } from '@molecule/api-bond';
|
|
8
|
+
const logger = getLogger();
|
|
9
|
+
import { findMany } from '@molecule/api-database';
|
|
10
|
+
import { t } from '@molecule/api-i18n';
|
|
11
|
+
/**
|
|
12
|
+
* The resource query Zod schema.
|
|
13
|
+
*/
|
|
14
|
+
export const querySchema = z.object({
|
|
15
|
+
limit: z.coerce.number().min(1).max(10000).default(100),
|
|
16
|
+
orderBy: z.enum([`createdAt`, `updatedAt`]).default(`updatedAt`),
|
|
17
|
+
orderDirection: z.enum([`asc`, `desc`]).default(`desc`),
|
|
18
|
+
before: z
|
|
19
|
+
.object({
|
|
20
|
+
createdAt: z.string().optional(),
|
|
21
|
+
updatedAt: z.string().optional(),
|
|
22
|
+
})
|
|
23
|
+
.optional(),
|
|
24
|
+
after: z
|
|
25
|
+
.object({
|
|
26
|
+
createdAt: z.string().optional(),
|
|
27
|
+
updatedAt: z.string().optional(),
|
|
28
|
+
})
|
|
29
|
+
.optional(),
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* Creates a request handler to be used to query resources.
|
|
33
|
+
*
|
|
34
|
+
* Uses the bonded DataStore from `@molecule/api-database` for database-agnostic operations.
|
|
35
|
+
* @param resource - The resource descriptor.
|
|
36
|
+
* @param resource.tableName - Database table to query resources from.
|
|
37
|
+
* @returns A curried async handler that accepts a `MoleculeRequest` (with query params for pagination/sorting) and returns a `{ statusCode, body }` response.
|
|
38
|
+
*/
|
|
39
|
+
export const query = ({ tableName, }) => async (req) => {
|
|
40
|
+
try {
|
|
41
|
+
// Validate the `req.query`.
|
|
42
|
+
const result = querySchema.safeParse(req.query);
|
|
43
|
+
if (!result.success) {
|
|
44
|
+
// The provided `req.query` failed validation.
|
|
45
|
+
const errors = result.error.issues
|
|
46
|
+
.map((e) => `Query.${e.path.join('.')}: ${e.message}`)
|
|
47
|
+
.join(', ');
|
|
48
|
+
return { statusCode: 400, body: { error: errors, errorKey: 'resource.error.badRequest' } };
|
|
49
|
+
}
|
|
50
|
+
const query = result.data;
|
|
51
|
+
// Build database-agnostic where conditions.
|
|
52
|
+
const where = [];
|
|
53
|
+
if (query.before) {
|
|
54
|
+
for (const key in query.before) {
|
|
55
|
+
if (query.before[key]) {
|
|
56
|
+
where.push({
|
|
57
|
+
field: key,
|
|
58
|
+
operator: '<',
|
|
59
|
+
value: query.before[key],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (query.after) {
|
|
65
|
+
for (const key in query.after) {
|
|
66
|
+
if (query.after[key]) {
|
|
67
|
+
where.push({
|
|
68
|
+
field: key,
|
|
69
|
+
operator: '>',
|
|
70
|
+
value: query.after[key],
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Build database-agnostic ordering.
|
|
76
|
+
const orderBy = [{ field: query.orderBy, direction: query.orderDirection }];
|
|
77
|
+
// Query the database via the bonded DataStore.
|
|
78
|
+
const resources = await findMany(tableName, {
|
|
79
|
+
where: where.length > 0 ? where : undefined,
|
|
80
|
+
orderBy,
|
|
81
|
+
limit: query.limit,
|
|
82
|
+
});
|
|
83
|
+
if (Array.isArray(resources)) {
|
|
84
|
+
// A valid response here should always be an array.
|
|
85
|
+
return { statusCode: 200, body: resources };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
logger.error(error);
|
|
90
|
+
}
|
|
91
|
+
// If we made it here, something isn't quite right.
|
|
92
|
+
return {
|
|
93
|
+
statusCode: 400,
|
|
94
|
+
body: {
|
|
95
|
+
error: t('resource.error.badRequest', undefined, { defaultValue: 'Bad request.' }),
|
|
96
|
+
errorKey: 'resource.error.badRequest',
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
//# sourceMappingURL=query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"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"}
|
package/dist/read.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource read operation.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type * as types from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Creates a handler to read a resource by its `id`.
|
|
9
|
+
*
|
|
10
|
+
* Uses the bonded DataStore from `@molecule/api-database` for database-agnostic operations.
|
|
11
|
+
*
|
|
12
|
+
* Example usage:
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { read as resourceRead } from '@molecule/api-resource'
|
|
15
|
+
* import type * as types from '../../types.js'
|
|
16
|
+
*
|
|
17
|
+
* export const read = ({ name, tableName, schema }: types.Resource) => {
|
|
18
|
+
* const read = resourceRead({ name, tableName, schema })
|
|
19
|
+
*
|
|
20
|
+
* return async (req: MoleculeRequest) => {
|
|
21
|
+
* const id = req.params.id
|
|
22
|
+
* return await read({ id })
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
* @param resource - The resource descriptor.
|
|
27
|
+
* @param resource.tableName - Database table to query the resource from.
|
|
28
|
+
* @returns A curried async function that accepts `{ id, props? }` and returns a `{ statusCode, body }` response.
|
|
29
|
+
*/
|
|
30
|
+
export declare const read: <ReadProps extends types.Props = types.Props, ReadResource extends types.Resource<ReadProps> = types.Resource<ReadProps>>({ tableName, }: ReadResource) => ({ id, props, }: {
|
|
31
|
+
/**
|
|
32
|
+
* The `id` of the resource to be retrieved from the database.
|
|
33
|
+
*/
|
|
34
|
+
id: ReadProps["id"];
|
|
35
|
+
/**
|
|
36
|
+
* If you already have the resource's props (e.g., an authorizer assigned them to context),
|
|
37
|
+
* you can avoid another database query by passing them through.
|
|
38
|
+
*
|
|
39
|
+
* Otherwise, the props will be retrieved from the database using the `id`.
|
|
40
|
+
*/
|
|
41
|
+
props?: ReadProps;
|
|
42
|
+
}) => Promise<{
|
|
43
|
+
statusCode: number;
|
|
44
|
+
body: {
|
|
45
|
+
props: ReadProps;
|
|
46
|
+
error?: undefined;
|
|
47
|
+
errorKey?: undefined;
|
|
48
|
+
};
|
|
49
|
+
} | {
|
|
50
|
+
statusCode: number;
|
|
51
|
+
body: {
|
|
52
|
+
error: string;
|
|
53
|
+
errorKey: string;
|
|
54
|
+
props?: undefined;
|
|
55
|
+
};
|
|
56
|
+
}>;
|
|
57
|
+
//# sourceMappingURL=read.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../src/read.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAExC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,IAAI,GAEb,SAAS,SAAS,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,EAC3C,YAAY,SAAS,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,EAC1E,gBAEC,YAAY,MACR,gBAGJ;IACD;;OAEG;IACH,EAAE,EAAE,SAAS,CAAC,IAAI,CAAC,CAAA;IACnB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,SAAS,CAAA;CAClB;;;;;;;;;;;;;;EAuBA,CAAA"}
|
package/dist/read.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource read operation.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { getLogger } from '@molecule/api-bond';
|
|
7
|
+
const logger = getLogger();
|
|
8
|
+
import { findById } from '@molecule/api-database';
|
|
9
|
+
import { t } from '@molecule/api-i18n';
|
|
10
|
+
/**
|
|
11
|
+
* Creates a handler to read a resource by its `id`.
|
|
12
|
+
*
|
|
13
|
+
* Uses the bonded DataStore from `@molecule/api-database` for database-agnostic operations.
|
|
14
|
+
*
|
|
15
|
+
* Example usage:
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { read as resourceRead } from '@molecule/api-resource'
|
|
18
|
+
* import type * as types from '../../types.js'
|
|
19
|
+
*
|
|
20
|
+
* export const read = ({ name, tableName, schema }: types.Resource) => {
|
|
21
|
+
* const read = resourceRead({ name, tableName, schema })
|
|
22
|
+
*
|
|
23
|
+
* return async (req: MoleculeRequest) => {
|
|
24
|
+
* const id = req.params.id
|
|
25
|
+
* return await read({ id })
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
* @param resource - The resource descriptor.
|
|
30
|
+
* @param resource.tableName - Database table to query the resource from.
|
|
31
|
+
* @returns A curried async function that accepts `{ id, props? }` and returns a `{ statusCode, body }` response.
|
|
32
|
+
*/
|
|
33
|
+
export const read = ({ tableName, }) => async ({ id, props, }) => {
|
|
34
|
+
try {
|
|
35
|
+
if (!props || props.id !== id) {
|
|
36
|
+
// Read the resource from the database via the bonded DataStore.
|
|
37
|
+
props = (await findById(tableName, id)) ?? undefined;
|
|
38
|
+
}
|
|
39
|
+
if (props && props.id === id) {
|
|
40
|
+
// Found the resource.
|
|
41
|
+
return { statusCode: 200, body: { props } };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
logger.error(error);
|
|
46
|
+
}
|
|
47
|
+
// If we made it here, the resource could not be found with the provided `id`.
|
|
48
|
+
return {
|
|
49
|
+
statusCode: 404,
|
|
50
|
+
body: {
|
|
51
|
+
error: t('resource.error.notFound', undefined, { defaultValue: 'Not found.' }),
|
|
52
|
+
errorKey: 'resource.error.notFound',
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=read.js.map
|
package/dist/read.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read.js","sourceRoot":"","sources":["../src/read.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;AAC1B,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAA;AAItC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,MAAM,IAAI,GACf,CAGE,EACA,SAAS,GACI,EAAE,EAAE,CACnB,KAAK,EAAE,EACL,EAAE,EACF,KAAK,GAaN,EAAE,EAAE;IACH,IAAI,CAAC;QACH,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;YAC9B,gEAAgE;YAChE,KAAK,GAAG,CAAC,MAAM,QAAQ,CAAY,SAAS,EAAE,EAAE,CAAC,CAAC,IAAI,SAAS,CAAA;QACjE,CAAC;QAED,IAAI,KAAK,IAAI,KAAK,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;YAC7B,sBAAsB;YACtB,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,CAAA;QAC7C,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IACrB,CAAC;IAED,8EAA8E;IAC9E,OAAO;QACL,UAAU,EAAE,GAAG;QACf,IAAI,EAAE;YACJ,KAAK,EAAE,CAAC,CAAC,yBAAyB,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,CAAC;YAC9E,QAAQ,EAAE,yBAAyB;SACpC;KACF,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared error responder for resource handlers.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type { MoleculeResponse } from './http-types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Fallback HTTP response used when a caught error is NOT a molecule-tagged error
|
|
9
|
+
* (i.e. a genuinely-unexpected failure).
|
|
10
|
+
*/
|
|
11
|
+
export interface ErrorFallback {
|
|
12
|
+
/** HTTP status for the fallback (typically 500). */
|
|
13
|
+
status: number;
|
|
14
|
+
/** Human-readable fallback message (already localized by the caller). */
|
|
15
|
+
message: string;
|
|
16
|
+
/** Machine-readable key the frontend maps to a localized message. */
|
|
17
|
+
errorKey: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Respond to a caught handler error, honoring molecule's "tagged error" convention.
|
|
21
|
+
*
|
|
22
|
+
* A tagged error carries BOTH a numeric `statusCode` AND a string `errorKey` — the same
|
|
23
|
+
* contract `@molecule/api-server-default-express`'s error middleware (`classifyTaggedError`)
|
|
24
|
+
* uses. Providers throw these for expected, user-actionable conditions — e.g. a
|
|
25
|
+
* config-missing throw (`503` + `'config.notConfigured'`) when `STRIPE_SECRET_KEY` is unset.
|
|
26
|
+
*
|
|
27
|
+
* Handlers that catch errors MUST funnel through this instead of a blanket
|
|
28
|
+
* `res.status(500)`, so a tagged error surfaces its REAL status + errorKey rather than
|
|
29
|
+
* being flattened to a generic 500. Otherwise the app looks broken at the exact moment the
|
|
30
|
+
* user tries to use the feature — the worst possible time for a payment (the conversion
|
|
31
|
+
* path). A swallowed tag is why the middleware-only fix was insufficient: ~185 handlers
|
|
32
|
+
* catch-and-500, so the tag never reached the middleware.
|
|
33
|
+
*
|
|
34
|
+
* Tagged errors are logged at `warn` (expected/actionable, not a server fault); everything
|
|
35
|
+
* else is logged at `error` and returns the caller's `fallback`. Requiring BOTH fields is
|
|
36
|
+
* deliberate: it keeps arbitrary library errors that merely carry a `.statusCode` (e.g. an
|
|
37
|
+
* AWS SDK 403) from being surfaced with a status molecule never chose.
|
|
38
|
+
*
|
|
39
|
+
* @param res - The response object.
|
|
40
|
+
* @param error - The caught error.
|
|
41
|
+
* @param fallback - Status/message/errorKey to use when `error` isn't a tagged error.
|
|
42
|
+
*/
|
|
43
|
+
export declare function respondError(res: MoleculeResponse, error: unknown, fallback: ErrorFallback): void;
|
|
44
|
+
//# sourceMappingURL=respond-error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"respond-error.d.ts","sourceRoot":"","sources":["../src/respond-error.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAIvD;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAA;IACd,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAA;IACf,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,gBAAgB,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,GAAG,IAAI,CAejG"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared error responder for resource handlers.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { getLogger } from '@molecule/api-bond';
|
|
7
|
+
const logger = getLogger();
|
|
8
|
+
/**
|
|
9
|
+
* Respond to a caught handler error, honoring molecule's "tagged error" convention.
|
|
10
|
+
*
|
|
11
|
+
* A tagged error carries BOTH a numeric `statusCode` AND a string `errorKey` — the same
|
|
12
|
+
* contract `@molecule/api-server-default-express`'s error middleware (`classifyTaggedError`)
|
|
13
|
+
* uses. Providers throw these for expected, user-actionable conditions — e.g. a
|
|
14
|
+
* config-missing throw (`503` + `'config.notConfigured'`) when `STRIPE_SECRET_KEY` is unset.
|
|
15
|
+
*
|
|
16
|
+
* Handlers that catch errors MUST funnel through this instead of a blanket
|
|
17
|
+
* `res.status(500)`, so a tagged error surfaces its REAL status + errorKey rather than
|
|
18
|
+
* being flattened to a generic 500. Otherwise the app looks broken at the exact moment the
|
|
19
|
+
* user tries to use the feature — the worst possible time for a payment (the conversion
|
|
20
|
+
* path). A swallowed tag is why the middleware-only fix was insufficient: ~185 handlers
|
|
21
|
+
* catch-and-500, so the tag never reached the middleware.
|
|
22
|
+
*
|
|
23
|
+
* Tagged errors are logged at `warn` (expected/actionable, not a server fault); everything
|
|
24
|
+
* else is logged at `error` and returns the caller's `fallback`. Requiring BOTH fields is
|
|
25
|
+
* deliberate: it keeps arbitrary library errors that merely carry a `.statusCode` (e.g. an
|
|
26
|
+
* AWS SDK 403) from being surfaced with a status molecule never chose.
|
|
27
|
+
*
|
|
28
|
+
* @param res - The response object.
|
|
29
|
+
* @param error - The caught error.
|
|
30
|
+
* @param fallback - Status/message/errorKey to use when `error` isn't a tagged error.
|
|
31
|
+
*/
|
|
32
|
+
export function respondError(res, error, fallback) {
|
|
33
|
+
if (typeof error === 'object' &&
|
|
34
|
+
error !== null &&
|
|
35
|
+
typeof error.statusCode === 'number' &&
|
|
36
|
+
typeof error.errorKey === 'string') {
|
|
37
|
+
const tagged = error;
|
|
38
|
+
const message = typeof tagged.message === 'string' ? tagged.message : fallback.message;
|
|
39
|
+
logger.warn(message, { errorKey: tagged.errorKey });
|
|
40
|
+
res.status(tagged.statusCode).json({ error: message, errorKey: tagged.errorKey });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
logger.error(fallback.message, { error });
|
|
44
|
+
res.status(fallback.status).json({ error: fallback.message, errorKey: fallback.errorKey });
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=respond-error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"respond-error.js","sourceRoot":"","sources":["../src/respond-error.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAI9C,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;AAe1B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,YAAY,CAAC,GAAqB,EAAE,KAAc,EAAE,QAAuB;IACzF,IACE,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,OAAQ,KAAkC,CAAC,UAAU,KAAK,QAAQ;QAClE,OAAQ,KAAgC,CAAC,QAAQ,KAAK,QAAQ,EAC9D,CAAC;QACD,MAAM,MAAM,GAAG,KAAoE,CAAA;QACnF,MAAM,OAAO,GAAG,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAA;QACtF,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAA;QACnD,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAA;QACjF,OAAM;IACR,CAAC;IACD,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;IACzC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAA;AAC5F,CAAC"}
|