@jskit-ai/crud-server-generator 0.1.26 → 0.1.28

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,234 +0,0 @@
1
- import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
2
- import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
3
- import {
4
- cursorPaginationQueryValidator,
5
- recordIdParamsValidator
6
- } from "@jskit-ai/kernel/shared/validators";
7
- import { routeParamsValidator } from "@jskit-ai/users-core/server/validators/routeParamsValidator";
8
- import { normalizeScopedRouteVisibility } from "@jskit-ai/users-core/shared/support/usersVisibility";
9
- import { buildWorkspaceInputFromRouteParams } from "@jskit-ai/users-core/server/support/workspaceRouteInput";
10
- import { resolveApiBasePath } from "@jskit-ai/users-core/shared/support/usersApiPaths";
11
- import { crudResource } from "../shared/crud/crudResource.js";
12
-
13
- function joinRoutePath(basePath = "", suffix = "") {
14
- const base = String(basePath || "").trim().replace(/\/+$/g, "");
15
- const end = String(suffix || "").trim();
16
- if (!end) {
17
- return base;
18
- }
19
-
20
- return `${base}/${end.replace(/^\/+/, "")}`;
21
- }
22
-
23
- function requireRouteRelativePath(routeRelativePath) {
24
- const routePath = String(routeRelativePath || "").trim();
25
- if (!routePath) {
26
- throw new TypeError("registerRoutes requires routeRelativePath.");
27
- }
28
-
29
- return routePath;
30
- }
31
-
32
- function requireActionIds(actionIds) {
33
- const source = actionIds && typeof actionIds === "object" && !Array.isArray(actionIds) ? actionIds : null;
34
- if (!source) {
35
- throw new TypeError("registerRoutes requires actionIds.");
36
- }
37
-
38
- const requiredKeys = ["list", "view", "create", "update", "delete"];
39
- const normalized = {};
40
- for (const key of requiredKeys) {
41
- const value = String(source[key] || "").trim();
42
- if (!value) {
43
- throw new TypeError(`registerRoutes requires actionIds.${key}.`);
44
- }
45
- normalized[key] = value;
46
- }
47
-
48
- return Object.freeze(normalized);
49
- }
50
-
51
- function registerRoutes(
52
- app,
53
- {
54
- routeRelativePath,
55
- routeOwnershipFilter = "public",
56
- routeSurface = "",
57
- routeSurfaceRequiresWorkspace = false,
58
- actionIds
59
- } = {}
60
- ) {
61
- if (!app || typeof app.make !== "function") {
62
- throw new Error("registerRoutes requires application make().");
63
- }
64
-
65
- const router = app.make("jskit.http.router");
66
- const relativePath = requireRouteRelativePath(routeRelativePath);
67
- const routeBase = resolveApiBasePath({
68
- surfaceRequiresWorkspace: routeSurfaceRequiresWorkspace === true,
69
- relativePath
70
- });
71
- const routeVisibility = normalizeScopedRouteVisibility(routeOwnershipFilter, {
72
- fallback: "public"
73
- });
74
- const surface = normalizeSurfaceId(routeSurface);
75
- const resolvedActionIds = requireActionIds(actionIds);
76
-
77
- router.register(
78
- "GET",
79
- routeBase,
80
- {
81
- auth: "required",
82
- surface,
83
- visibility: routeVisibility,
84
- meta: {
85
- tags: ["crud"],
86
- summary: "List records."
87
- },
88
- paramsValidator: routeParamsValidator,
89
- queryValidator: cursorPaginationQueryValidator,
90
- responseValidators: withStandardErrorResponses({
91
- 200: crudResource.operations.list.outputValidator
92
- })
93
- },
94
- async function (request, reply) {
95
- const listInput = {
96
- ...buildWorkspaceInputFromRouteParams(request.input.params)
97
- };
98
- if (request.input.query.cursor != null) {
99
- listInput.cursor = request.input.query.cursor;
100
- }
101
- if (request.input.query.limit != null) {
102
- listInput.limit = request.input.query.limit;
103
- }
104
- const response = await request.executeAction({
105
- actionId: resolvedActionIds.list,
106
- input: listInput
107
- });
108
- reply.code(200).send(response);
109
- }
110
- );
111
-
112
- router.register(
113
- "GET",
114
- joinRoutePath(routeBase, ":recordId"),
115
- {
116
- auth: "required",
117
- surface,
118
- visibility: routeVisibility,
119
- meta: {
120
- tags: ["crud"],
121
- summary: "View a record."
122
- },
123
- paramsValidator: [routeParamsValidator, recordIdParamsValidator],
124
- responseValidators: withStandardErrorResponses({
125
- 200: crudResource.operations.view.outputValidator
126
- })
127
- },
128
- async function (request, reply) {
129
- const response = await request.executeAction({
130
- actionId: resolvedActionIds.view,
131
- input: {
132
- ...buildWorkspaceInputFromRouteParams(request.input.params),
133
- recordId: request.input.params.recordId
134
- }
135
- });
136
- reply.code(200).send(response);
137
- }
138
- );
139
-
140
- router.register(
141
- "POST",
142
- routeBase,
143
- {
144
- auth: "required",
145
- surface,
146
- visibility: routeVisibility,
147
- meta: {
148
- tags: ["crud"],
149
- summary: "Create a record."
150
- },
151
- paramsValidator: routeParamsValidator,
152
- bodyValidator: crudResource.operations.create.bodyValidator,
153
- responseValidators: withStandardErrorResponses(
154
- {
155
- 201: crudResource.operations.create.outputValidator
156
- },
157
- { includeValidation400: true }
158
- )
159
- },
160
- async function (request, reply) {
161
- const response = await request.executeAction({
162
- actionId: resolvedActionIds.create,
163
- input: {
164
- ...buildWorkspaceInputFromRouteParams(request.input.params),
165
- payload: request.input.body
166
- }
167
- });
168
- reply.code(201).send(response);
169
- }
170
- );
171
-
172
- router.register(
173
- "PATCH",
174
- joinRoutePath(routeBase, ":recordId"),
175
- {
176
- auth: "required",
177
- surface,
178
- visibility: routeVisibility,
179
- meta: {
180
- tags: ["crud"],
181
- summary: "Update a record."
182
- },
183
- paramsValidator: [routeParamsValidator, recordIdParamsValidator],
184
- bodyValidator: crudResource.operations.patch.bodyValidator,
185
- responseValidators: withStandardErrorResponses(
186
- {
187
- 200: crudResource.operations.patch.outputValidator
188
- },
189
- { includeValidation400: true }
190
- )
191
- },
192
- async function (request, reply) {
193
- const response = await request.executeAction({
194
- actionId: resolvedActionIds.update,
195
- input: {
196
- ...buildWorkspaceInputFromRouteParams(request.input.params),
197
- recordId: request.input.params.recordId,
198
- patch: request.input.body
199
- }
200
- });
201
- reply.code(200).send(response);
202
- }
203
- );
204
-
205
- router.register(
206
- "DELETE",
207
- joinRoutePath(routeBase, ":recordId"),
208
- {
209
- auth: "required",
210
- surface,
211
- visibility: routeVisibility,
212
- meta: {
213
- tags: ["crud"],
214
- summary: "Delete a record."
215
- },
216
- paramsValidator: [routeParamsValidator, recordIdParamsValidator],
217
- responseValidators: withStandardErrorResponses({
218
- 200: crudResource.operations.delete.outputValidator
219
- })
220
- },
221
- async function (request, reply) {
222
- const response = await request.executeAction({
223
- actionId: resolvedActionIds.delete,
224
- input: {
225
- ...buildWorkspaceInputFromRouteParams(request.input.params),
226
- recordId: request.input.params.recordId
227
- }
228
- });
229
- reply.code(200).send(response);
230
- }
231
- );
232
- }
233
-
234
- export { registerRoutes };
@@ -1,162 +0,0 @@
1
- import { toInsertDateTime } from "@jskit-ai/database-runtime/shared";
2
- import { applyVisibility, applyVisibilityOwners } from "@jskit-ai/database-runtime/shared/visibility";
3
- import {
4
- DEFAULT_LIST_LIMIT,
5
- normalizeCrudListLimit,
6
- requireCrudTableName
7
- } from "@jskit-ai/crud-core/server/repositorySupport";
8
- import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators/inputNormalization";
9
- import { pickOwnProperties } from "@jskit-ai/kernel/shared/support";
10
-
11
- function mapRecordRow(row) {
12
- if (!row) {
13
- return null;
14
- }
15
-
16
- return {
17
- id: row.id,
18
- textField: row.text_field,
19
- dateField: row.date_field,
20
- numberField: row.number_field,
21
- createdAt: row.created_at,
22
- updatedAt: row.updated_at
23
- };
24
- }
25
-
26
- function createRepository(knex, { tableName } = {}) {
27
- if (typeof knex !== "function") {
28
- throw new TypeError("crudRepository requires knex.");
29
- }
30
-
31
- const resolvedTableName = requireCrudTableName(tableName);
32
-
33
- async function list({ cursor = 0, limit = DEFAULT_LIST_LIMIT } = {}, options = {}) {
34
- const client = options?.trx || knex;
35
- const normalizedCursor = Number.isInteger(Number(cursor)) && Number(cursor) > 0 ? Number(cursor) : 0;
36
- const normalizedLimit = normalizeCrudListLimit(limit);
37
- const visible = (queryBuilder) => applyVisibility(queryBuilder, options.visibilityContext);
38
-
39
- let query = client(resolvedTableName)
40
- .select("id", "text_field", "date_field", "number_field", "created_at", "updated_at")
41
- .where(visible)
42
- .orderBy("id", "asc")
43
- .limit(normalizedLimit + 1);
44
-
45
- if (normalizedCursor > 0) {
46
- query = query.where("id", ">", normalizedCursor);
47
- }
48
-
49
- const rows = await query;
50
- const hasMore = rows.length > normalizedLimit;
51
- const pageRows = hasMore ? rows.slice(0, normalizedLimit) : rows;
52
- const items = pageRows.map((row) => mapRecordRow(row));
53
-
54
- return {
55
- items,
56
- nextCursor: hasMore && items.length > 0 ? String(items[items.length - 1].id) : null
57
- };
58
- }
59
-
60
- async function findById(recordId, options = {}) {
61
- const client = options?.trx || knex;
62
- const visible = (queryBuilder) => applyVisibility(queryBuilder, options.visibilityContext);
63
- const row = await client(resolvedTableName)
64
- .select("id", "text_field", "date_field", "number_field", "created_at", "updated_at")
65
- .where(visible)
66
- .where({ id: Number(recordId) })
67
- .first();
68
-
69
- return mapRecordRow(row);
70
- }
71
-
72
- async function create(payload = {}, options = {}) {
73
- const client = options?.trx || knex;
74
- const source = normalizeObjectInput(payload);
75
- const timestamp = toInsertDateTime();
76
- const insertPayload = applyVisibilityOwners(
77
- {
78
- text_field: source.textField,
79
- date_field: source.dateField,
80
- number_field: source.numberField,
81
- created_at: timestamp,
82
- updated_at: timestamp
83
- },
84
- options.visibilityContext
85
- );
86
- const [recordId] = await client(resolvedTableName).insert({
87
- ...insertPayload
88
- });
89
-
90
- return findById(recordId, {
91
- ...options,
92
- trx: client
93
- });
94
- }
95
-
96
- async function updateById(recordId, patch = {}, options = {}) {
97
- const client = options?.trx || knex;
98
- const source = normalizeObjectInput(patch);
99
- const dbPatch = {};
100
- const patchSource = pickOwnProperties(source, ["textField", "dateField", "numberField"]);
101
- if (Object.hasOwn(patchSource, "textField")) {
102
- dbPatch.text_field = patchSource.textField;
103
- }
104
- if (Object.hasOwn(patchSource, "dateField")) {
105
- dbPatch.date_field = patchSource.dateField;
106
- }
107
- if (Object.hasOwn(patchSource, "numberField")) {
108
- dbPatch.number_field = patchSource.numberField;
109
- }
110
- const visible = (queryBuilder) => applyVisibility(queryBuilder, options.visibilityContext);
111
-
112
- if (Object.keys(dbPatch).length === 0) {
113
- return findById(recordId, {
114
- ...options,
115
- trx: client
116
- });
117
- }
118
-
119
- await client(resolvedTableName)
120
- .where(visible)
121
- .where({ id: Number(recordId) })
122
- .update({
123
- ...dbPatch,
124
- updated_at: toInsertDateTime()
125
- });
126
-
127
- return findById(recordId, {
128
- ...options,
129
- trx: client
130
- });
131
- }
132
-
133
- async function deleteById(recordId, options = {}) {
134
- const client = options?.trx || knex;
135
- const visible = (queryBuilder) => applyVisibility(queryBuilder, options.visibilityContext);
136
- const existing = await findById(recordId, {
137
- ...options,
138
- trx: client
139
- });
140
-
141
- if (!existing) {
142
- return null;
143
- }
144
-
145
- await client(resolvedTableName).where(visible).where({ id: Number(recordId) }).delete();
146
-
147
- return {
148
- id: existing.id,
149
- deleted: true
150
- };
151
- }
152
-
153
- return Object.freeze({
154
- list,
155
- findById,
156
- create,
157
- updateById,
158
- deleteById
159
- });
160
- }
161
-
162
- export { createRepository };
@@ -1,96 +0,0 @@
1
- import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
-
3
- const serviceEvents = Object.freeze({
4
- createRecord: Object.freeze([
5
- Object.freeze({
6
- type: "entity.changed",
7
- source: "crud",
8
- entity: "record",
9
- operation: "created",
10
- entityId: ({ result }) => result?.id,
11
- realtime: Object.freeze({
12
- event: "crud.record.changed",
13
- audience: "event_scope"
14
- })
15
- })
16
- ]),
17
- updateRecord: Object.freeze([
18
- Object.freeze({
19
- type: "entity.changed",
20
- source: "crud",
21
- entity: "record",
22
- operation: "updated",
23
- entityId: ({ result }) => result?.id,
24
- realtime: Object.freeze({
25
- event: "crud.record.changed",
26
- audience: "event_scope"
27
- })
28
- })
29
- ]),
30
- deleteRecord: Object.freeze([
31
- Object.freeze({
32
- type: "entity.changed",
33
- source: "crud",
34
- entity: "record",
35
- operation: "deleted",
36
- entityId: ({ result }) => result?.id,
37
- realtime: Object.freeze({
38
- event: "crud.record.changed",
39
- audience: "event_scope"
40
- })
41
- })
42
- ])
43
- });
44
-
45
- function createService({ crudRepository } = {}) {
46
- if (!crudRepository) {
47
- throw new Error("crudService requires crudRepository.");
48
- }
49
-
50
- async function listRecords(query = {}, options = {}) {
51
- return crudRepository.list(query, options);
52
- }
53
-
54
- async function getRecord(recordId, options = {}) {
55
- const record = await crudRepository.findById(recordId, options);
56
- if (!record) {
57
- throw new AppError(404, "Record not found.");
58
- }
59
-
60
- return record;
61
- }
62
-
63
- async function createRecord(payload = {}, options = {}) {
64
- const record = await crudRepository.create(payload, options);
65
- if (!record) {
66
- throw new Error("crudService could not load the created record.");
67
- }
68
- return record;
69
- }
70
-
71
- async function updateRecord(recordId, payload = {}, options = {}) {
72
- const record = await crudRepository.updateById(recordId, payload, options);
73
- if (!record) {
74
- throw new AppError(404, "Record not found.");
75
- }
76
- return record;
77
- }
78
-
79
- async function deleteRecord(recordId, options = {}) {
80
- const deleted = await crudRepository.deleteById(recordId, options);
81
- if (!deleted) {
82
- throw new AppError(404, "Record not found.");
83
- }
84
- return deleted;
85
- }
86
-
87
- return Object.freeze({
88
- listRecords,
89
- getRecord,
90
- createRecord,
91
- updateRecord,
92
- deleteRecord
93
- });
94
- }
95
-
96
- export { createService, serviceEvents };