@jskit-ai/crud-server-generator 0.1.26

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.
@@ -0,0 +1,67 @@
1
+ export default Object.freeze({
2
+ packageVersion: 1,
3
+ packageId: "@local/${option:namespace|kebab}",
4
+ version: "0.1.0",
5
+ kind: "runtime",
6
+ description: "App-local CRUD package (${option:namespace|kebab}).",
7
+ dependsOn: [
8
+ "@jskit-ai/auth-core",
9
+ "@jskit-ai/crud-core",
10
+ "@jskit-ai/database-runtime",
11
+ "@jskit-ai/http-runtime",
12
+ "@jskit-ai/realtime",
13
+ "@jskit-ai/users-core"
14
+ ],
15
+ capabilities: {
16
+ provides: [
17
+ "crud.${option:namespace|kebab}"
18
+ ],
19
+ requires: [
20
+ "runtime.actions",
21
+ "runtime.database",
22
+ "auth.policy",
23
+ "users.core"
24
+ ]
25
+ },
26
+ runtime: {
27
+ server: {
28
+ providers: [
29
+ {
30
+ entrypoint: "src/server/${option:namespace|pascal}ServiceProvider.js",
31
+ export: "${option:namespace|pascal}ServiceProvider"
32
+ }
33
+ ]
34
+ }
35
+ },
36
+ metadata: {
37
+ apiSummary: {
38
+ surfaces: [
39
+ {
40
+ subpath: "./server/actionIds",
41
+ summary: "App-local CRUD public action identifiers."
42
+ },
43
+ {
44
+ subpath: "./shared",
45
+ summary: "App-local CRUD shared resource."
46
+ }
47
+ ],
48
+ containerTokens: {
49
+ server: [
50
+ "repository.${option:namespace|snake}",
51
+ "crud.${option:namespace|snake}"
52
+ ]
53
+ }
54
+ }
55
+ },
56
+ mutations: {
57
+ dependencies: {
58
+ runtime: {},
59
+ dev: {}
60
+ },
61
+ packageJson: {
62
+ scripts: {}
63
+ },
64
+ procfile: {},
65
+ files: []
66
+ }
67
+ });
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@local/${option:namespace|kebab}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ "./server/actionIds": "./src/server/actionIds.js",
8
+ "./shared": "./src/shared/index.js"
9
+ }
10
+ }
@@ -0,0 +1,84 @@
1
+ import { resolveAppConfig } from "@jskit-ai/kernel/server/support";
2
+ import { resolveCrudSurfacePolicyFromAppConfig } from "@jskit-ai/crud-core/server/crudModuleConfig";
3
+ import { withActionDefaults } from "@jskit-ai/kernel/shared/actions";
4
+ import { createRepository } from "./repository.js";
5
+ import {
6
+ createService,
7
+ serviceEvents
8
+ } from "./service.js";
9
+ import { createActions } from "./actions.js";
10
+ import { registerRoutes } from "./registerRoutes.js";
11
+ const NAMESPACE_${option:namespace|snake|upper}_TABLE_NAME = __JSKIT_CRUD_TABLE_NAME__;
12
+ const NAMESPACE_${option:namespace|snake|upper}_ID_COLUMN = __JSKIT_CRUD_ID_COLUMN__;
13
+ const CRUD_MODULE_CONFIG = Object.freeze({
14
+ namespace: "${option:namespace|snake}",
15
+ surface: "${option:surface|lower}",
16
+ ownershipFilter: "__JSKIT_CRUD_RESOLVED_OWNERSHIP_FILTER__",
17
+ relativePath: "/${option:directory-prefix|pathprefix}${option:namespace|kebab}"
18
+ });
19
+
20
+ function resolveCrudPolicyFromApp(app) {
21
+ return resolveCrudSurfacePolicyFromAppConfig(CRUD_MODULE_CONFIG, resolveAppConfig(app), {
22
+ context: "${option:namespace|pascal}ServiceProvider"
23
+ });
24
+ }
25
+
26
+ class ${option:namespace|pascal}ServiceProvider {
27
+ static id = "crud.${option:namespace|snake}";
28
+
29
+ static dependsOn = ["runtime.actions", "runtime.database", "auth.policy.fastify", "local.main", "users.core"];
30
+
31
+ register(app) {
32
+ if (!app || typeof app.singleton !== "function" || typeof app.service !== "function" || typeof app.actions !== "function") {
33
+ throw new Error("${option:namespace|pascal}ServiceProvider requires application singleton()/service()/actions().");
34
+ }
35
+
36
+ const crudPolicy = resolveCrudPolicyFromApp(app);
37
+
38
+ app.singleton("repository.${option:namespace|snake}", (scope) => {
39
+ const knex = scope.make("jskit.database.knex");
40
+ return createRepository(knex, {
41
+ tableName: NAMESPACE_${option:namespace|snake|upper}_TABLE_NAME,
42
+ idColumn: NAMESPACE_${option:namespace|snake|upper}_ID_COLUMN
43
+ });
44
+ });
45
+
46
+ app.service(
47
+ "crud.${option:namespace|snake}",
48
+ (scope) => {
49
+ return createService({
50
+ ${option:namespace|camel}Repository: scope.make("repository.${option:namespace|snake}")
51
+ });
52
+ },
53
+ {
54
+ events: serviceEvents
55
+ }
56
+ );
57
+
58
+ app.actions(
59
+ withActionDefaults(
60
+ createActions({
61
+ surface: crudPolicy.surfaceId
62
+ }),
63
+ {
64
+ domain: "crud",
65
+ dependencies: {
66
+ ${option:namespace|camel}Service: "crud.${option:namespace|snake}"
67
+ }
68
+ }
69
+ )
70
+ );
71
+ }
72
+
73
+ boot(app) {
74
+ const crudPolicy = resolveCrudPolicyFromApp(app);
75
+ registerRoutes(app, {
76
+ routeOwnershipFilter: crudPolicy.ownershipFilter,
77
+ routeSurface: crudPolicy.surfaceId,
78
+ routeSurfaceRequiresWorkspace: crudPolicy.surfaceDefinition.requiresWorkspace === true,
79
+ routeRelativePath: crudPolicy.relativePath
80
+ });
81
+ }
82
+ }
83
+
84
+ export { ${option:namespace|pascal}ServiceProvider };
@@ -0,0 +1,9 @@
1
+ const actionIds = Object.freeze({
2
+ list: "crud.${option:namespace|snake}.list",
3
+ view: "crud.${option:namespace|snake}.view",
4
+ create: "crud.${option:namespace|snake}.create",
5
+ update: "crud.${option:namespace|snake}.update",
6
+ delete: "crud.${option:namespace|snake}.delete"
7
+ });
8
+
9
+ export { actionIds };
@@ -0,0 +1,151 @@
1
+ import {
2
+ cursorPaginationQueryValidator,
3
+ recordIdParamsValidator
4
+ } from "@jskit-ai/kernel/shared/validators";
5
+ import { workspaceSlugParamsValidator } from "@jskit-ai/users-core/server/validators/routeParamsValidator";
6
+ import { ${option:namespace|singular|camel}Resource } from "../shared/${option:namespace|singular|camel}Resource.js";
7
+ import { actionIds } from "./actionIds.js";
8
+
9
+ function requireActionSurface(surface = "") {
10
+ const normalizedSurface = String(surface || "").trim().toLowerCase();
11
+ if (!normalizedSurface) {
12
+ throw new TypeError("createActions requires a non-empty surface.");
13
+ }
14
+
15
+ return normalizedSurface;
16
+ }
17
+
18
+ function createActions({ surface = "" } = {}) {
19
+ const actionSurface = requireActionSurface(surface);
20
+
21
+ return Object.freeze([
22
+ {
23
+ id: actionIds.list,
24
+ version: 1,
25
+ kind: "query",
26
+ channels: ["api", "automation", "internal"],
27
+ surfaces: [actionSurface],
28
+ permission: {
29
+ require: "authenticated"
30
+ },
31
+ inputValidator: [workspaceSlugParamsValidator, cursorPaginationQueryValidator],
32
+ outputValidator: ${option:namespace|singular|camel}Resource.operations.list.outputValidator,
33
+ idempotency: "none",
34
+ audit: {
35
+ actionName: actionIds.list
36
+ },
37
+ observability: {},
38
+ async execute(input, context, deps) {
39
+ return deps.${option:namespace|camel}Service.listRecords(input, {
40
+ context,
41
+ visibilityContext: context?.visibilityContext
42
+ });
43
+ }
44
+ },
45
+ {
46
+ id: actionIds.view,
47
+ version: 1,
48
+ kind: "query",
49
+ channels: ["api", "automation", "internal"],
50
+ surfaces: [actionSurface],
51
+ permission: {
52
+ require: "authenticated"
53
+ },
54
+ inputValidator: [workspaceSlugParamsValidator, recordIdParamsValidator],
55
+ outputValidator: ${option:namespace|singular|camel}Resource.operations.view.outputValidator,
56
+ idempotency: "none",
57
+ audit: {
58
+ actionName: actionIds.view
59
+ },
60
+ observability: {},
61
+ async execute(input, context, deps) {
62
+ return deps.${option:namespace|camel}Service.getRecord(input.recordId, {
63
+ context,
64
+ visibilityContext: context?.visibilityContext
65
+ });
66
+ }
67
+ },
68
+ {
69
+ id: actionIds.create,
70
+ version: 1,
71
+ kind: "command",
72
+ channels: ["api", "automation", "internal"],
73
+ surfaces: [actionSurface],
74
+ permission: {
75
+ require: "authenticated"
76
+ },
77
+ inputValidator: [
78
+ workspaceSlugParamsValidator,
79
+ {
80
+ payload: ${option:namespace|singular|camel}Resource.operations.create.bodyValidator
81
+ }
82
+ ],
83
+ outputValidator: ${option:namespace|singular|camel}Resource.operations.create.outputValidator,
84
+ idempotency: "optional",
85
+ audit: {
86
+ actionName: actionIds.create
87
+ },
88
+ observability: {},
89
+ async execute(input, context, deps) {
90
+ return deps.${option:namespace|camel}Service.createRecord(input.payload, {
91
+ context,
92
+ visibilityContext: context?.visibilityContext
93
+ });
94
+ }
95
+ },
96
+ {
97
+ id: actionIds.update,
98
+ version: 1,
99
+ kind: "command",
100
+ channels: ["api", "automation", "internal"],
101
+ surfaces: [actionSurface],
102
+ permission: {
103
+ require: "authenticated"
104
+ },
105
+ inputValidator: [
106
+ workspaceSlugParamsValidator,
107
+ recordIdParamsValidator,
108
+ {
109
+ patch: ${option:namespace|singular|camel}Resource.operations.patch.bodyValidator
110
+ }
111
+ ],
112
+ outputValidator: ${option:namespace|singular|camel}Resource.operations.patch.outputValidator,
113
+ idempotency: "optional",
114
+ audit: {
115
+ actionName: actionIds.update
116
+ },
117
+ observability: {},
118
+ async execute(input, context, deps) {
119
+ return deps.${option:namespace|camel}Service.updateRecord(input.recordId, input.patch, {
120
+ context,
121
+ visibilityContext: context?.visibilityContext
122
+ });
123
+ }
124
+ },
125
+ {
126
+ id: actionIds.delete,
127
+ version: 1,
128
+ kind: "command",
129
+ channels: ["api", "automation", "internal"],
130
+ surfaces: [actionSurface],
131
+ permission: {
132
+ require: "authenticated"
133
+ },
134
+ inputValidator: [workspaceSlugParamsValidator, recordIdParamsValidator],
135
+ outputValidator: ${option:namespace|singular|camel}Resource.operations.delete.outputValidator,
136
+ idempotency: "optional",
137
+ audit: {
138
+ actionName: actionIds.delete
139
+ },
140
+ observability: {},
141
+ async execute(input, context, deps) {
142
+ return deps.${option:namespace|camel}Service.deleteRecord(input.recordId, {
143
+ context,
144
+ visibilityContext: context?.visibilityContext
145
+ });
146
+ }
147
+ }
148
+ ]);
149
+ }
150
+
151
+ export { createActions };
@@ -0,0 +1,197 @@
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 { actionIds } from "./actionIds.js";
12
+ import { ${option:namespace|singular|camel}Resource } from "../shared/${option:namespace|singular|camel}Resource.js";
13
+
14
+ function registerRoutes(
15
+ app,
16
+ {
17
+ routeOwnershipFilter = "public",
18
+ routeSurface = "",
19
+ routeSurfaceRequiresWorkspace = false,
20
+ routeRelativePath = ""
21
+ } = {}
22
+ ) {
23
+ if (!app || typeof app.make !== "function") {
24
+ throw new Error("registerRoutes requires application make().");
25
+ }
26
+ if (!String(routeRelativePath || "").trim()) {
27
+ throw new Error("registerRoutes requires routeRelativePath.");
28
+ }
29
+
30
+ const router = app.make("jskit.http.router");
31
+ const routeVisibility = normalizeScopedRouteVisibility(routeOwnershipFilter, {
32
+ fallback: "public"
33
+ });
34
+ const normalizedRouteSurface = normalizeSurfaceId(routeSurface);
35
+ const routeBase = resolveApiBasePath({
36
+ surfaceRequiresWorkspace: routeSurfaceRequiresWorkspace === true,
37
+ relativePath: routeRelativePath
38
+ });
39
+
40
+ router.register(
41
+ "GET",
42
+ routeBase,
43
+ {
44
+ auth: "required",
45
+ surface: normalizedRouteSurface,
46
+ visibility: routeVisibility,
47
+ meta: {
48
+ tags: ["crud"],
49
+ summary: "List records."
50
+ },
51
+ paramsValidator: routeParamsValidator,
52
+ queryValidator: cursorPaginationQueryValidator,
53
+ responseValidators: withStandardErrorResponses({
54
+ 200: ${option:namespace|singular|camel}Resource.operations.list.outputValidator
55
+ })
56
+ },
57
+ async function (request, reply) {
58
+ const listInput = {
59
+ ...buildWorkspaceInputFromRouteParams(request.input.params)
60
+ };
61
+ if (request.input.query.cursor != null) {
62
+ listInput.cursor = request.input.query.cursor;
63
+ }
64
+ if (request.input.query.limit != null) {
65
+ listInput.limit = request.input.query.limit;
66
+ }
67
+ const response = await request.executeAction({
68
+ actionId: actionIds.list,
69
+ input: listInput
70
+ });
71
+ reply.code(200).send(response);
72
+ }
73
+ );
74
+
75
+ router.register(
76
+ "GET",
77
+ `${routeBase}/:recordId`,
78
+ {
79
+ auth: "required",
80
+ surface: normalizedRouteSurface,
81
+ visibility: routeVisibility,
82
+ meta: {
83
+ tags: ["crud"],
84
+ summary: "View a record."
85
+ },
86
+ paramsValidator: [routeParamsValidator, recordIdParamsValidator],
87
+ responseValidators: withStandardErrorResponses({
88
+ 200: ${option:namespace|singular|camel}Resource.operations.view.outputValidator
89
+ })
90
+ },
91
+ async function (request, reply) {
92
+ const response = await request.executeAction({
93
+ actionId: actionIds.view,
94
+ input: {
95
+ ...buildWorkspaceInputFromRouteParams(request.input.params),
96
+ recordId: request.input.params.recordId
97
+ }
98
+ });
99
+ reply.code(200).send(response);
100
+ }
101
+ );
102
+
103
+ router.register(
104
+ "POST",
105
+ routeBase,
106
+ {
107
+ auth: "required",
108
+ surface: normalizedRouteSurface,
109
+ visibility: routeVisibility,
110
+ meta: {
111
+ tags: ["crud"],
112
+ summary: "Create a record."
113
+ },
114
+ paramsValidator: routeParamsValidator,
115
+ bodyValidator: ${option:namespace|singular|camel}Resource.operations.create.bodyValidator,
116
+ responseValidators: withStandardErrorResponses(
117
+ {
118
+ 201: ${option:namespace|singular|camel}Resource.operations.create.outputValidator
119
+ },
120
+ { includeValidation400: true }
121
+ )
122
+ },
123
+ async function (request, reply) {
124
+ const response = await request.executeAction({
125
+ actionId: actionIds.create,
126
+ input: {
127
+ ...buildWorkspaceInputFromRouteParams(request.input.params),
128
+ payload: request.input.body
129
+ }
130
+ });
131
+ reply.code(201).send(response);
132
+ }
133
+ );
134
+
135
+ router.register(
136
+ "PATCH",
137
+ `${routeBase}/:recordId`,
138
+ {
139
+ auth: "required",
140
+ surface: normalizedRouteSurface,
141
+ visibility: routeVisibility,
142
+ meta: {
143
+ tags: ["crud"],
144
+ summary: "Update a record."
145
+ },
146
+ paramsValidator: [routeParamsValidator, recordIdParamsValidator],
147
+ bodyValidator: ${option:namespace|singular|camel}Resource.operations.patch.bodyValidator,
148
+ responseValidators: withStandardErrorResponses(
149
+ {
150
+ 200: ${option:namespace|singular|camel}Resource.operations.patch.outputValidator
151
+ },
152
+ { includeValidation400: true }
153
+ )
154
+ },
155
+ async function (request, reply) {
156
+ const response = await request.executeAction({
157
+ actionId: actionIds.update,
158
+ input: {
159
+ ...buildWorkspaceInputFromRouteParams(request.input.params),
160
+ recordId: request.input.params.recordId,
161
+ patch: request.input.body
162
+ }
163
+ });
164
+ reply.code(200).send(response);
165
+ }
166
+ );
167
+
168
+ router.register(
169
+ "DELETE",
170
+ `${routeBase}/:recordId`,
171
+ {
172
+ auth: "required",
173
+ surface: normalizedRouteSurface,
174
+ visibility: routeVisibility,
175
+ meta: {
176
+ tags: ["crud"],
177
+ summary: "Delete a record."
178
+ },
179
+ paramsValidator: [routeParamsValidator, recordIdParamsValidator],
180
+ responseValidators: withStandardErrorResponses({
181
+ 200: ${option:namespace|singular|camel}Resource.operations.delete.outputValidator
182
+ })
183
+ },
184
+ async function (request, reply) {
185
+ const response = await request.executeAction({
186
+ actionId: actionIds.delete,
187
+ input: {
188
+ ...buildWorkspaceInputFromRouteParams(request.input.params),
189
+ recordId: request.input.params.recordId
190
+ }
191
+ });
192
+ reply.code(200).send(response);
193
+ }
194
+ );
195
+ }
196
+
197
+ export { registerRoutes };
@@ -0,0 +1,161 @@
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
+ buildWritePayload as baseBuildWritePayload,
8
+ mapRecordRow as baseMapRecordRow,
9
+ resolveColumnName,
10
+ resolveCrudIdColumn
11
+ } from "@jskit-ai/crud-core/server/repositorySupport";
12
+
13
+ const DEFAULT_ID_COLUMN = __JSKIT_CRUD_ID_COLUMN__;
14
+ const OUTPUT_KEYS = Object.freeze(__JSKIT_CRUD_REPOSITORY_OUTPUT_KEYS__);
15
+ const WRITE_KEYS = Object.freeze(__JSKIT_CRUD_REPOSITORY_WRITE_KEYS__);
16
+ const COLUMN_OVERRIDES = Object.freeze(__JSKIT_CRUD_REPOSITORY_COLUMN_OVERRIDES__);
17
+ const CREATED_AT_COLUMN = __JSKIT_CRUD_REPOSITORY_CREATED_AT_COLUMN__;
18
+ const UPDATED_AT_COLUMN = __JSKIT_CRUD_REPOSITORY_UPDATED_AT_COLUMN__;
19
+
20
+ const {
21
+ selectColumns: SELECT_COLUMNS,
22
+ outputMappings: OUTPUT_MAPPINGS,
23
+ writeMappings: WRITE_MAPPINGS
24
+ } = buildRepositoryColumnMetadata({
25
+ outputKeys: OUTPUT_KEYS,
26
+ writeKeys: WRITE_KEYS,
27
+ columnOverrides: COLUMN_OVERRIDES
28
+ });
29
+
30
+ function createRepository(knex, { tableName, idColumn = DEFAULT_ID_COLUMN } = {}) {
31
+ if (typeof knex !== "function") {
32
+ throw new TypeError("crudRepository requires knex.");
33
+ }
34
+
35
+ const resolvedTableName = requireCrudTableName(tableName);
36
+ const resolvedIdColumn = resolveCrudIdColumn(idColumn, { fallback: DEFAULT_ID_COLUMN });
37
+
38
+ async function list({ cursor = 0, limit = DEFAULT_LIST_LIMIT } = {}, options = {}) {
39
+ const client = options?.trx || knex;
40
+ const normalizedCursor = Number.isInteger(Number(cursor)) && Number(cursor) > 0 ? Number(cursor) : 0;
41
+ const normalizedLimit = normalizeCrudListLimit(limit);
42
+ const visible = (queryBuilder) => applyVisibility(queryBuilder, options.visibilityContext);
43
+
44
+ let query = client(resolvedTableName)
45
+ .select(...SELECT_COLUMNS)
46
+ .where(visible)
47
+ .orderBy(resolvedIdColumn, "asc")
48
+ .limit(normalizedLimit + 1);
49
+
50
+ if (normalizedCursor > 0) {
51
+ query = query.where(resolvedIdColumn, ">", normalizedCursor);
52
+ }
53
+
54
+ const rows = await query;
55
+ const hasMore = rows.length > normalizedLimit;
56
+ const pageRows = hasMore ? rows.slice(0, normalizedLimit) : rows;
57
+ const items = pageRows.map((row) => baseMapRecordRow(row, OUTPUT_KEYS, COLUMN_OVERRIDES));
58
+
59
+ return {
60
+ items,
61
+ nextCursor: hasMore && items.length > 0 ? String(items[items.length - 1].id) : null
62
+ };
63
+ }
64
+
65
+ async function findById(recordId, options = {}) {
66
+ const client = options?.trx || knex;
67
+ const visible = (queryBuilder) => applyVisibility(queryBuilder, options.visibilityContext);
68
+ const row = await client(resolvedTableName)
69
+ .select(...SELECT_COLUMNS)
70
+ .where(visible)
71
+ .where({
72
+ [resolvedIdColumn]: Number(recordId)
73
+ })
74
+ .first();
75
+
76
+ return baseMapRecordRow(row, OUTPUT_KEYS, COLUMN_OVERRIDES);
77
+ }
78
+
79
+ async function create(payload = {}, options = {}) {
80
+ const client = options?.trx || knex;
81
+ const timestamp = toInsertDateTime();
82
+ const insertPayload = baseBuildWritePayload(payload, WRITE_KEYS, COLUMN_OVERRIDES);
83
+ if (CREATED_AT_COLUMN && !Object.hasOwn(insertPayload, CREATED_AT_COLUMN)) {
84
+ insertPayload[CREATED_AT_COLUMN] = timestamp;
85
+ }
86
+ if (UPDATED_AT_COLUMN && !Object.hasOwn(insertPayload, UPDATED_AT_COLUMN)) {
87
+ insertPayload[UPDATED_AT_COLUMN] = timestamp;
88
+ }
89
+
90
+ const withOwners = applyVisibilityOwners(insertPayload, options.visibilityContext);
91
+ const [recordId] = await client(resolvedTableName).insert(withOwners);
92
+
93
+ return findById(recordId, {
94
+ ...options,
95
+ trx: client
96
+ });
97
+ }
98
+
99
+ async function updateById(recordId, patch = {}, options = {}) {
100
+ const client = options?.trx || knex;
101
+ const dbPatch = baseBuildWritePayload(patch, WRITE_KEYS, COLUMN_OVERRIDES);
102
+ const visible = (queryBuilder) => applyVisibility(queryBuilder, options.visibilityContext);
103
+ if (UPDATED_AT_COLUMN) {
104
+ dbPatch[UPDATED_AT_COLUMN] = toInsertDateTime();
105
+ }
106
+
107
+ if (Object.keys(dbPatch).length < 1) {
108
+ return findById(recordId, {
109
+ ...options,
110
+ trx: client
111
+ });
112
+ }
113
+
114
+ await client(resolvedTableName)
115
+ .where(visible)
116
+ .where({
117
+ [resolvedIdColumn]: Number(recordId)
118
+ })
119
+ .update(dbPatch);
120
+
121
+ return findById(recordId, {
122
+ ...options,
123
+ trx: client
124
+ });
125
+ }
126
+
127
+ async function deleteById(recordId, options = {}) {
128
+ const client = options?.trx || knex;
129
+ const visible = (queryBuilder) => applyVisibility(queryBuilder, options.visibilityContext);
130
+ const existing = await findById(recordId, {
131
+ ...options,
132
+ trx: client
133
+ });
134
+
135
+ if (!existing) {
136
+ return null;
137
+ }
138
+
139
+ await client(resolvedTableName)
140
+ .where(visible)
141
+ .where({
142
+ [resolvedIdColumn]: Number(recordId)
143
+ })
144
+ .delete();
145
+
146
+ return {
147
+ id: existing.id,
148
+ deleted: true
149
+ };
150
+ }
151
+
152
+ return Object.freeze({
153
+ list,
154
+ findById,
155
+ create,
156
+ updateById,
157
+ deleteById
158
+ });
159
+ }
160
+
161
+ export { createRepository };