@jskit-ai/crud-server-generator 0.1.63 → 0.1.64

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.
@@ -1,23 +1,20 @@
1
- import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
2
1
  import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
3
- import {
4
- createCrudCursorPaginationQueryValidator,
5
- listSearchQueryValidator,
6
- lookupIncludeQueryValidator,
7
- createCrudParentFilterQueryValidator
8
- } from "@jskit-ai/crud-core/server/listQueryValidators";
9
- import {
10
- recordIdParamsValidator
11
- } from "@jskit-ai/kernel/shared/validators";
2
+ import { createCrudJsonApiRouteContracts } from "@jskit-ai/crud-core/server/routeContracts";
12
3
  import { checkRouteVisibility } from "@jskit-ai/kernel/shared/support/visibility";
13
4
  import { resolveScopedApiBasePath } from "@jskit-ai/kernel/shared/surface";
14
- import { actionIds } from "./actionIds.js";
15
5
  import { resource } from "../shared/${option:namespace|singular|camel}Resource.js";
16
- import { LIST_CONFIG } from "./listConfig.js";
17
6
  __JSKIT_CRUD_ROUTE_WORKSPACE_SUPPORT_IMPORTS__
18
7
 
19
- const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator(LIST_CONFIG);
20
- const listParentFilterQueryValidator = createCrudParentFilterQueryValidator(resource);
8
+ const {
9
+ listRouteContract,
10
+ viewRouteContract,
11
+ createRouteContract,
12
+ updateRouteContract,
13
+ deleteRouteContract,
14
+ recordRouteParamsValidator
15
+ } = createCrudJsonApiRouteContracts({
16
+ resource__JSKIT_CRUD_ROUTE_CONTRACTS_RESOURCE_ARGS__
17
+ });
21
18
 
22
19
  function registerRoutes(
23
20
  app,
@@ -46,23 +43,15 @@ function registerRoutes(
46
43
  tags: ["crud"],
47
44
  summary: "List records."
48
45
  },
46
+ ...listRouteContract,
49
47
  __JSKIT_CRUD_LIST_ROUTE_PARAMS_VALIDATOR_LINE__
50
- queryValidator: [
51
- listCursorPaginationQueryValidator,
52
- listSearchQueryValidator,
53
- listParentFilterQueryValidator,
54
- lookupIncludeQueryValidator
55
- ],
56
- responseValidators: withStandardErrorResponses({
57
- 200: resource.operations.list.outputValidator
58
- })
59
48
  },
60
49
  async function (request, reply) {
61
50
  const listInput = {
62
51
  __JSKIT_CRUD_LIST_ROUTE_INPUT_LINES__
63
52
  };
64
53
  const response = await request.executeAction({
65
- actionId: actionIds.list,
54
+ actionId: "crud.${option:namespace|snake}.list",
66
55
  input: listInput
67
56
  });
68
57
  reply.code(200).send(response);
@@ -80,15 +69,12 @@ __JSKIT_CRUD_LIST_ROUTE_INPUT_LINES__
80
69
  tags: ["crud"],
81
70
  summary: "View a record."
82
71
  },
72
+ ...viewRouteContract,
83
73
  __JSKIT_CRUD_VIEW_ROUTE_PARAMS_VALIDATOR_LINE__
84
- queryValidator: [lookupIncludeQueryValidator],
85
- responseValidators: withStandardErrorResponses({
86
- 200: resource.operations.view.outputValidator
87
- })
88
74
  },
89
75
  async function (request, reply) {
90
76
  const response = await request.executeAction({
91
- actionId: actionIds.view,
77
+ actionId: "crud.${option:namespace|snake}.view",
92
78
  input: {
93
79
  __JSKIT_CRUD_VIEW_ROUTE_INPUT_LINES__
94
80
  }
@@ -108,18 +94,12 @@ __JSKIT_CRUD_VIEW_ROUTE_INPUT_LINES__
108
94
  tags: ["crud"],
109
95
  summary: "Create a record."
110
96
  },
97
+ ...createRouteContract,
111
98
  __JSKIT_CRUD_CREATE_ROUTE_PARAMS_VALIDATOR_LINE__
112
- bodyValidator: resource.operations.create.bodyValidator,
113
- responseValidators: withStandardErrorResponses(
114
- {
115
- 201: resource.operations.create.outputValidator
116
- },
117
- { includeValidation400: true }
118
- )
119
99
  },
120
100
  async function (request, reply) {
121
101
  const response = await request.executeAction({
122
- actionId: actionIds.create,
102
+ actionId: "crud.${option:namespace|snake}.create",
123
103
  input: {
124
104
  __JSKIT_CRUD_CREATE_ROUTE_INPUT_LINES__
125
105
  }
@@ -139,18 +119,12 @@ __JSKIT_CRUD_CREATE_ROUTE_INPUT_LINES__
139
119
  tags: ["crud"],
140
120
  summary: "Update a record."
141
121
  },
122
+ ...updateRouteContract,
142
123
  __JSKIT_CRUD_UPDATE_ROUTE_PARAMS_VALIDATOR_LINE__
143
- bodyValidator: resource.operations.patch.bodyValidator,
144
- responseValidators: withStandardErrorResponses(
145
- {
146
- 200: resource.operations.patch.outputValidator
147
- },
148
- { includeValidation400: true }
149
- )
150
124
  },
151
125
  async function (request, reply) {
152
126
  const response = await request.executeAction({
153
- actionId: actionIds.update,
127
+ actionId: "crud.${option:namespace|snake}.update",
154
128
  input: {
155
129
  __JSKIT_CRUD_UPDATE_ROUTE_INPUT_LINES__
156
130
  }
@@ -170,19 +144,17 @@ __JSKIT_CRUD_UPDATE_ROUTE_INPUT_LINES__
170
144
  tags: ["crud"],
171
145
  summary: "Delete a record."
172
146
  },
147
+ ...deleteRouteContract,
173
148
  __JSKIT_CRUD_DELETE_ROUTE_PARAMS_VALIDATOR_LINE__
174
- responseValidators: withStandardErrorResponses({
175
- 200: resource.operations.delete.outputValidator
176
- })
177
149
  },
178
150
  async function (request, reply) {
179
151
  const response = await request.executeAction({
180
- actionId: actionIds.delete,
152
+ actionId: "crud.${option:namespace|snake}.delete",
181
153
  input: {
182
154
  __JSKIT_CRUD_DELETE_ROUTE_INPUT_LINES__
183
155
  }
184
156
  });
185
- reply.code(200).send(response);
157
+ reply.code(204).send(response);
186
158
  }
187
159
  );
188
160
  }
@@ -1,57 +1,101 @@
1
- import { createCrudResourceRuntime } from "@jskit-ai/crud-core/server/resourceRuntime";
1
+ import { createWithTransaction } from "@jskit-ai/database-runtime/shared";
2
+ import {
3
+ buildJsonRestQueryParams,
4
+ createJsonApiInputRecord,
5
+ createJsonRestContext,
6
+ returnNullWhenJsonRestResourceMissing
7
+ } from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
2
8
  import { resource } from "../shared/${option:namespace|singular|camel}Resource.js";
3
- import { LIST_CONFIG } from "./listConfig.js";
9
+ const RESOURCE_TYPE = resource.namespace;
4
10
 
5
- const REPOSITORY_CONTEXT = "${option:namespace|snake} repository";
11
+ function createRepository({ api, knex } = {}) {
12
+ const withTransaction = createWithTransaction(knex);
6
13
 
7
- const REPOSITORY_CONFIG = Object.freeze({
8
- context: REPOSITORY_CONTEXT,
9
- list: LIST_CONFIG
10
- });
11
-
12
- function createRepository(knex, options = {}) {
13
- const resourceRuntime = createCrudResourceRuntime(resource, knex, {
14
- ...options,
15
- ...REPOSITORY_CONFIG
16
- });
17
-
18
- async function list(query = {}, callOptions = {}) {
19
- return resourceRuntime.list(query, callOptions);
14
+ async function queryDocuments(query = {}, options = {}) {
15
+ return api.resources.${option:namespace|camel}.query(
16
+ {
17
+ queryParams: buildJsonRestQueryParams(RESOURCE_TYPE, query),
18
+ transaction: options?.trx || null,
19
+ simplified: false
20
+ },
21
+ createJsonRestContext(options?.context || null)
22
+ );
20
23
  }
21
24
 
22
- async function findById(recordId, callOptions = {}) {
23
- return resourceRuntime.findById(recordId, callOptions);
25
+ async function getDocumentById(recordId, options = {}) {
26
+ return returnNullWhenJsonRestResourceMissing(() =>
27
+ api.resources.${option:namespace|camel}.get(
28
+ {
29
+ id: recordId,
30
+ queryParams: buildJsonRestQueryParams(RESOURCE_TYPE, {}, {
31
+ include: options?.include
32
+ }),
33
+ transaction: options?.trx || null,
34
+ simplified: false
35
+ },
36
+ createJsonRestContext(options?.context || null)
37
+ )
38
+ );
24
39
  }
25
40
 
26
- async function listByIds(ids = [], callOptions = {}) {
27
- return resourceRuntime.listByIds(ids, callOptions);
41
+ async function createDocument(payload = {}, options = {}) {
42
+ return api.resources.${option:namespace|camel}.post(
43
+ {
44
+ inputRecord: createJsonApiInputRecord(RESOURCE_TYPE, payload),
45
+ transaction: options?.trx || null,
46
+ simplified: false
47
+ },
48
+ createJsonRestContext(options?.context || null)
49
+ );
28
50
  }
29
51
 
30
- async function listByForeignIds(ids = [], foreignKey = "", callOptions = {}) {
31
- return resourceRuntime.listByForeignIds(ids, foreignKey, callOptions);
32
- }
52
+ async function patchDocumentById(recordId, patch = {}, options = {}) {
53
+ const sourcePatch = patch && typeof patch === "object" && !Array.isArray(patch) ? patch : {};
54
+ if (Object.keys(sourcePatch).length < 1) {
55
+ return getDocumentById(recordId, options);
56
+ }
33
57
 
34
- async function create(payload = {}, callOptions = {}) {
35
- return resourceRuntime.create(payload, callOptions);
58
+ return returnNullWhenJsonRestResourceMissing(() =>
59
+ api.resources.${option:namespace|camel}.patch(
60
+ {
61
+ id: recordId,
62
+ inputRecord: createJsonApiInputRecord(
63
+ RESOURCE_TYPE,
64
+ {
65
+ ...sourcePatch,
66
+ updatedAt: new Date()
67
+ }
68
+ ),
69
+ transaction: options?.trx || null,
70
+ simplified: false
71
+ },
72
+ createJsonRestContext(options?.context || null)
73
+ )
74
+ );
36
75
  }
37
76
 
38
- async function updateById(recordId, patch = {}, callOptions = {}) {
39
- return resourceRuntime.updateById(recordId, patch, callOptions);
40
- }
77
+ async function deleteDocumentById(recordId, options = {}) {
78
+ return returnNullWhenJsonRestResourceMissing(async () => {
79
+ await api.resources.${option:namespace|camel}.delete(
80
+ {
81
+ id: recordId,
82
+ transaction: options?.trx || null,
83
+ simplified: false
84
+ },
85
+ createJsonRestContext(options?.context || null)
86
+ );
41
87
 
42
- async function deleteById(recordId, callOptions = {}) {
43
- return resourceRuntime.deleteById(recordId, callOptions);
88
+ return true;
89
+ });
44
90
  }
45
91
 
46
92
  return Object.freeze({
47
- withTransaction: resourceRuntime.withTransaction,
48
- list,
49
- findById,
50
- listByIds,
51
- listByForeignIds,
52
- create,
53
- updateById,
54
- deleteById
93
+ withTransaction,
94
+ queryDocuments,
95
+ getDocumentById,
96
+ createDocument,
97
+ patchDocumentById,
98
+ deleteDocumentById
55
99
  });
56
100
  }
57
101
 
@@ -1,90 +1,62 @@
1
- import { createCrudServiceEvents } from "@jskit-ai/crud-core/server/serviceEvents";
2
- import {
3
- createCrudServiceRuntime,
4
- crudServiceListRecords,
5
- crudServiceGetRecord,
6
- crudServiceCreateRecord,
7
- crudServiceUpdateRecord,
8
- crudServiceDeleteRecord
9
- } from "@jskit-ai/crud-core/server/serviceMethods";
10
- import { resource } from "../shared/${option:namespace|singular|camel}Resource.js";
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
+ import { returnJsonApiDocument } from "@jskit-ai/http-runtime/shared";
11
3
 
12
- const serviceRuntime = createCrudServiceRuntime(resource, {
13
- context: "${option:namespace|camel}Service"
14
- });
15
- const baseServiceEvents = createCrudServiceEvents(resource, {
16
- context: "${option:namespace|camel}Service"
17
- });
18
-
19
- const serviceEvents = Object.freeze({
20
- createRecord: [...baseServiceEvents.createRecord],
21
- updateRecord: [...baseServiceEvents.updateRecord],
22
- deleteRecord: [...baseServiceEvents.deleteRecord]
23
- });
4
+ function return404IfNotFound(document = null) {
5
+ if (!document) {
6
+ throw new AppError(404, "Document not found.");
7
+ }
8
+ return document;
9
+ }
24
10
 
25
- const DEFAULT_FIELD_ACCESS = Object.freeze({
26
- // Tip: use createFieldAccessForRoleMatrix(...) from @jskit-ai/crud-core/server/fieldAccess to centralize role matrices.
27
- // Example:
28
- // const DEFAULT_FIELD_ACCESS = createFieldAccessForRoleMatrix({
29
- // default: {
30
- // readable: { list: ["id", "name"], view: ["id", "name", "email"] },
31
- // writable: { create: ["name", "email"], update: ["name"] }
32
- // },
33
- // admin: {
34
- // readable: "*",
35
- // writable: "*"
36
- // },
37
- // writeMode: "throw" // or "strip"
38
- // });
39
- // readable: ({ action, context }) => ["id", "name"], // null/"*" means no read filtering
40
- // Read redaction behavior: drop optional fields; use null/default for required fields.
41
- // writable: ({ action, context }) => ["name"], // null/"*" means no write filtering
42
- // writeMode: "throw" // "throw" (default) or "strip"
43
- });
11
+ function createService({ ${option:namespace|camel}Repository } = {}) {
12
+ if (!${option:namespace|camel}Repository) {
13
+ throw new TypeError("createService requires ${option:namespace|camel}Repository.");
14
+ }
44
15
 
45
- function createService({ ${option:namespace|camel}Repository, fieldAccess = DEFAULT_FIELD_ACCESS } = {}) {
46
- async function listRecords(query = {}, options = {}) {
47
- return crudServiceListRecords(serviceRuntime, ${option:namespace|camel}Repository, fieldAccess, query, options);
16
+ async function queryDocuments(query = {}, options = {}) {
17
+ return returnJsonApiDocument(await ${option:namespace|camel}Repository.queryDocuments(query, {
18
+ trx: options?.trx || null,
19
+ context: options?.context || null
20
+ }));
48
21
  }
49
22
 
50
- async function getRecord(recordId, options = {}) {
51
- return crudServiceGetRecord(serviceRuntime, ${option:namespace|camel}Repository, fieldAccess, recordId, options);
23
+ async function getDocumentById(recordId, options = {}) {
24
+ return returnJsonApiDocument(return404IfNotFound(await ${option:namespace|camel}Repository.getDocumentById(recordId, {
25
+ trx: options?.trx || null,
26
+ context: options?.context || null,
27
+ include: options?.include
28
+ })));
52
29
  }
53
30
 
54
- async function createRecord(payload = {}, options = {}) {
55
- return crudServiceCreateRecord(serviceRuntime, ${option:namespace|camel}Repository, fieldAccess, payload, options);
31
+ async function createDocument(payload = {}, options = {}) {
32
+ return returnJsonApiDocument(await ${option:namespace|camel}Repository.createDocument(payload, {
33
+ trx: options?.trx || null,
34
+ context: options?.context || null
35
+ }));
56
36
  }
57
37
 
58
- async function updateRecord(recordId, payload = {}, options = {}) {
59
- return crudServiceUpdateRecord(serviceRuntime, ${option:namespace|camel}Repository, fieldAccess, recordId, payload, options);
38
+ async function patchDocumentById(recordId, payload = {}, options = {}) {
39
+ return returnJsonApiDocument(return404IfNotFound(await ${option:namespace|camel}Repository.patchDocumentById(recordId, payload, {
40
+ trx: options?.trx || null,
41
+ context: options?.context || null
42
+ })));
60
43
  }
61
44
 
62
- async function deleteRecord(recordId, options = {}) {
63
- return crudServiceDeleteRecord(serviceRuntime, ${option:namespace|camel}Repository, fieldAccess, recordId, options);
45
+ async function deleteDocumentById(recordId, options = {}) {
46
+ return404IfNotFound(await ${option:namespace|camel}Repository.deleteDocumentById(recordId, {
47
+ trx: options?.trx || null,
48
+ context: options?.context || null
49
+ }));
50
+ return null;
64
51
  }
65
52
 
66
53
  return Object.freeze({
67
- listRecords,
68
- getRecord,
69
- createRecord,
70
- updateRecord,
71
- deleteRecord
54
+ queryDocuments,
55
+ getDocumentById,
56
+ createDocument,
57
+ patchDocumentById,
58
+ deleteDocumentById
72
59
  });
73
60
  }
74
61
 
75
- // Optional event override example:
76
- // const serviceEvents = {
77
- // ...baseServiceEvents,
78
- // createRecord: [
79
- // ...baseServiceEvents.createRecord,
80
- // {
81
- // type: "${option:namespace|snake}.custom",
82
- // source: "custom",
83
- // entity: "record",
84
- // operation: "created",
85
- // entityId: ({ result }) => result?.id
86
- // }
87
- // ]
88
- // };
89
-
90
- export { createService, serviceEvents };
62
+ export { createService };
@@ -1,91 +1,16 @@
1
- import { Type } from "typebox";
2
- __JSKIT_CRUD_RESOURCE_DATABASE_RUNTIME_IMPORT__
3
- __JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__
4
- __JSKIT_CRUD_RESOURCE_NORMALIZE_SUPPORT_IMPORT__
5
- __JSKIT_CRUD_RESOURCE_JSON_IMPORT__
1
+ import { defineCrudResource } from "@jskit-ai/resource-crud-core/shared/crudResource";
6
2
 
7
- const RESOURCE_LOOKUP_CONTAINER_KEY = "lookups";
8
-
9
- const recordOutputSchema = Type.Object(
10
- {
11
- __JSKIT_CRUD_RESOURCE_OUTPUT_SCHEMA_PROPERTIES__
12
- [RESOURCE_LOOKUP_CONTAINER_KEY]: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
13
- },
14
- { additionalProperties: false }
15
- );
16
-
17
- const createBodySchema = Type.Object(
18
- {
19
- __JSKIT_CRUD_RESOURCE_CREATE_SCHEMA_PROPERTIES__
20
- },
21
- {
22
- additionalProperties: false,
23
- required: __JSKIT_CRUD_RESOURCE_CREATE_REQUIRED_FIELDS__
24
- }
25
- );
26
-
27
- const patchBodySchema = Type.Partial(createBodySchema, {
28
- additionalProperties: false
29
- });
30
-
31
- const recordOutputValidator = Object.freeze({
32
- schema: recordOutputSchema,
33
- normalize(payload = {}) {
34
- const source = normalizeObjectInput(payload);
35
- const normalized = {
36
- __JSKIT_CRUD_RESOURCE_OUTPUT_NORMALIZATION_LINES__
37
- };
38
- if (Object.hasOwn(source, RESOURCE_LOOKUP_CONTAINER_KEY)) {
39
- normalized[RESOURCE_LOOKUP_CONTAINER_KEY] = source[RESOURCE_LOOKUP_CONTAINER_KEY];
40
- }
41
-
42
- return normalized;
43
- }
44
- });
45
-
46
- const listOutputValidator = createCursorListValidator(recordOutputValidator);
47
-
48
- const createBodyValidator = Object.freeze({
49
- schema: createBodySchema,
50
- normalize(payload = {}) {
51
- const source = normalizeObjectInput(payload);
52
- const normalized = {};
53
-
54
- __JSKIT_CRUD_RESOURCE_INPUT_NORMALIZATION_LINES__
55
-
56
- return normalized;
57
- }
58
- });
59
-
60
- const patchBodyValidator = Object.freeze({
61
- schema: patchBodySchema,
62
- normalize: createBodyValidator.normalize
63
- });
64
-
65
- const deleteOutputValidator = Object.freeze({
66
- schema: Type.Object(
67
- {
68
- id: recordIdSchema,
69
- deleted: Type.Literal(true)
70
- },
71
- { additionalProperties: false }
72
- ),
73
- normalize(payload = {}) {
74
- const source = normalizeObjectInput(payload);
75
-
76
- return {
77
- id: normalizeRecordId(source.id, { fallback: "" }),
78
- deleted: true
79
- };
80
- }
81
- });
82
-
83
- const RESOURCE_FIELD_META = [];
84
-
85
- const resource = {
3
+ const resource = defineCrudResource({
86
4
  namespace: "${option:namespace|snake}",
87
5
  tableName: __JSKIT_CRUD_TABLE_NAME__,
88
- idColumn: __JSKIT_CRUD_ID_COLUMN__,
6
+ schema: {
7
+ __JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__
8
+ },
9
+ searchSchema: {
10
+ __JSKIT_CRUD_RESOURCE_SEARCH_SCHEMA_LINES__
11
+ },
12
+ defaultSort: __JSKIT_CRUD_RESOURCE_DEFAULT_SORT__,
13
+ autofilter: __JSKIT_CRUD_RESOURCE_AUTOFILTER__,
89
14
  messages: {
90
15
  validation: "Fix invalid values and try again.",
91
16
  saveSuccess: "Record saved.",
@@ -95,61 +20,11 @@ const resource = {
95
20
  },
96
21
  contract: {
97
22
  lookup: {
98
- containerKey: RESOURCE_LOOKUP_CONTAINER_KEY,
99
- defaultInclude: "*", // Set "none" to disable lookup hydration unless include=... is passed.
100
- maxDepth: 3 // Lower this to limit nested lookup hydration depth.
101
- }
102
- },
103
- operations: {
104
- list: {
105
- realtime: {
106
- events: ["${option:namespace|snake}.record.changed"] // Add more events e.g. for lookup records
107
- },
108
- method: "GET",
109
- outputValidator: listOutputValidator
110
- },
111
- view: {
112
- method: "GET",
113
- outputValidator: recordOutputValidator
114
- },
115
- create: {
116
- method: "POST",
117
- bodyValidator: createBodyValidator,
118
- outputValidator: recordOutputValidator
119
- },
120
- patch: {
121
- method: "PATCH",
122
- bodyValidator: patchBodyValidator,
123
- outputValidator: recordOutputValidator
124
- },
125
- delete: {
126
- method: "DELETE",
127
- outputValidator: deleteOutputValidator
23
+ containerKey: "lookups",
24
+ defaultInclude: "*",
25
+ maxDepth: 3
128
26
  }
129
- },
130
- fieldMeta: RESOURCE_FIELD_META
131
- };
27
+ }
28
+ });
132
29
 
133
30
  export { resource };
134
-
135
- // @jskit-contract crud.resource.field-meta.${option:namespace|snake}.v1
136
- void RESOURCE_FIELD_META;
137
-
138
- // Example 1:n collection hydration:
139
- // RESOURCE_FIELD_META.push({
140
- // key: "pets",
141
- // relation: {
142
- // kind: "collection",
143
- // namespace: "pets",
144
- // foreignKey: "customerId",
145
- // parentValueKey: "id",
146
- // hydrateOnList: false, // list: opt-in with include=pets
147
- // hydrateOnView: true // view: hydrated by default
148
- // }
149
- // });
150
- //
151
- // To hydrate child lookups too, request nested include paths:
152
- // - include=pets
153
- // - include=pets,pets.breedId
154
-
155
- __JSKIT_CRUD_RESOURCE_FIELD_META_PUSH_LINES__