@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,41 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { crudResource } from "../src/shared/crud/crudResource.js";
4
+
5
+ test("crudResource normalizes create payload", () => {
6
+ const normalized = crudResource.operations.create.bodyValidator.normalize({
7
+ textField: " Example text ",
8
+ dateField: "2026-03-11",
9
+ numberField: "42.5"
10
+ });
11
+
12
+ assert.deepEqual(normalized, {
13
+ textField: "Example text",
14
+ dateField: "2026-03-11 00:00:00.000",
15
+ numberField: 42.5
16
+ });
17
+ });
18
+
19
+ test("crudResource normalizes list output", () => {
20
+ const normalized = crudResource.operations.list.outputValidator.normalize({
21
+ items: [
22
+ {
23
+ id: "7",
24
+ textField: " Example text ",
25
+ dateField: "2026-03-10",
26
+ numberField: "99",
27
+ createdAt: "2026-03-11 00:00:00.000",
28
+ updatedAt: "2026-03-11 00:00:00.000"
29
+ }
30
+ ],
31
+ nextCursor: " 8 "
32
+ });
33
+
34
+ assert.equal(normalized.items[0].id, 7);
35
+ assert.equal(normalized.items[0].textField, "Example text");
36
+ assert.equal(normalized.items[0].dateField, "2026-03-10T00:00:00.000Z");
37
+ assert.equal(normalized.items[0].numberField, 99);
38
+ assert.match(normalized.items[0].createdAt, /T/);
39
+ assert.match(normalized.items[0].updatedAt, /T/);
40
+ assert.equal(normalized.nextCursor, "8");
41
+ });
@@ -0,0 +1,61 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createActions } from "../src/server/actions.js";
4
+ import { createActionIds } from "../src/server/actionIds.js";
5
+ import { createRepository } from "../src/server/repository.js";
6
+ import { registerRoutes } from "../src/server/registerRoutes.js";
7
+
8
+ test("createActionIds requires explicit actionIdPrefix", () => {
9
+ assert.throws(
10
+ () => createActionIds(""),
11
+ /requires actionIdPrefix/
12
+ );
13
+ });
14
+
15
+ test("createRepository requires explicit tableName", () => {
16
+ const knex = () => {
17
+ throw new Error("not expected");
18
+ };
19
+
20
+ assert.throws(
21
+ () => createRepository(knex, {}),
22
+ /requires tableName/
23
+ );
24
+ });
25
+
26
+ test("createActions requires explicit surface", () => {
27
+ assert.throws(
28
+ () =>
29
+ createActions({
30
+ actionIdPrefix: "crud.customers"
31
+ }),
32
+ /requires a non-empty surface/
33
+ );
34
+ });
35
+
36
+ test("registerRoutes requires explicit routeRelativePath and actionIds", () => {
37
+ const app = {
38
+ make() {
39
+ return {
40
+ register() {}
41
+ };
42
+ }
43
+ };
44
+
45
+ assert.throws(
46
+ () => registerRoutes(app, {}),
47
+ /requires routeRelativePath/
48
+ );
49
+
50
+ assert.throws(
51
+ () =>
52
+ registerRoutes(app, {
53
+ routeRelativePath: "/customers",
54
+ routeSurfaceRequiresWorkspace: true,
55
+ actionIds: {
56
+ list: "crud.customers.list"
57
+ }
58
+ }),
59
+ /requires actionIds.view/
60
+ );
61
+ });
@@ -0,0 +1,83 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createService } from "../src/server/service.js";
4
+
5
+ test("crudService delegates CRUD operations to the repository", async () => {
6
+ const calls = [];
7
+ const crudRepository = {
8
+ async list(query) {
9
+ calls.push(["list", query]);
10
+ return { items: [], nextCursor: null };
11
+ },
12
+ async findById(recordId) {
13
+ calls.push(["findById", recordId]);
14
+ return { id: recordId, textField: "Example", dateField: "2026-03-11T00:00:00.000Z", numberField: 3 };
15
+ },
16
+ async create(payload) {
17
+ calls.push(["create", payload]);
18
+ return { id: 1, ...payload };
19
+ },
20
+ async updateById(recordId, payload) {
21
+ calls.push(["updateById", recordId, payload]);
22
+ return { id: recordId, ...payload };
23
+ },
24
+ async deleteById(recordId) {
25
+ calls.push(["deleteById", recordId]);
26
+ return { id: recordId, deleted: true };
27
+ }
28
+ };
29
+
30
+ const service = createService({ crudRepository });
31
+
32
+ const options = {};
33
+ await service.listRecords({ limit: 10 }, options);
34
+ await service.getRecord(3, options);
35
+ await service.createRecord({ textField: "Example", dateField: "2026-03-11", numberField: 3 }, options);
36
+ await service.updateRecord(4, { textField: "Changed" }, options);
37
+ await service.deleteRecord(5, options);
38
+
39
+ assert.deepEqual(calls, [
40
+ ["list", { limit: 10 }],
41
+ ["findById", 3],
42
+ ["create", { textField: "Example", dateField: "2026-03-11", numberField: 3 }],
43
+ ["updateById", 4, { textField: "Changed" }],
44
+ ["deleteById", 5]
45
+ ]);
46
+ });
47
+
48
+ test("crudService throws 404 when a record is missing", async () => {
49
+ const service = createService({
50
+ crudRepository: {
51
+ async list() {
52
+ return { items: [], nextCursor: null };
53
+ },
54
+ async findById() {
55
+ return null;
56
+ },
57
+ async create(payload) {
58
+ return { id: 1, ...payload };
59
+ },
60
+ async updateById() {
61
+ return null;
62
+ },
63
+ async deleteById() {
64
+ return null;
65
+ }
66
+ }
67
+ });
68
+
69
+ await assert.rejects(
70
+ () => service.getRecord(9, {}),
71
+ (error) => error?.status === 404 && error?.message === "Record not found."
72
+ );
73
+
74
+ await assert.rejects(
75
+ () => service.updateRecord(9, { textField: "Changed" }, {}),
76
+ (error) => error?.status === 404 && error?.message === "Record not found."
77
+ );
78
+
79
+ await assert.rejects(
80
+ () => service.deleteRecord(9, {}),
81
+ (error) => error?.status === 404 && error?.message === "Record not found."
82
+ );
83
+ });
@@ -0,0 +1,215 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { registerRoutes } from "../src/server/registerRoutes.js";
4
+ import { resolveApiBasePath } from "@jskit-ai/users-core/shared/support/usersApiPaths";
5
+
6
+ function createReplyDouble() {
7
+ return {
8
+ statusCode: 200,
9
+ payload: null,
10
+ code(statusCode) {
11
+ this.statusCode = statusCode;
12
+ return this;
13
+ },
14
+ send(payload) {
15
+ this.payload = payload;
16
+ return this;
17
+ }
18
+ };
19
+ }
20
+
21
+ function findRoute(routes, method, path) {
22
+ return routes.find((route) => route.method === method && route.path === path) || null;
23
+ }
24
+
25
+ test("crud routes build create/update action input with explicit payload and patch keys", async () => {
26
+ const registeredRoutes = [];
27
+ const router = {
28
+ register(method, path, route, handler) {
29
+ registeredRoutes.push({
30
+ method,
31
+ path,
32
+ route,
33
+ handler
34
+ });
35
+ }
36
+ };
37
+ const app = {
38
+ make(token) {
39
+ if (token !== "jskit.http.router") {
40
+ throw new Error(`Unexpected token: ${String(token)}`);
41
+ }
42
+ return router;
43
+ }
44
+ };
45
+
46
+ registerRoutes(app, {
47
+ routeRelativePath: "/customers",
48
+ routeSurfaceRequiresWorkspace: true,
49
+ actionIds: {
50
+ list: "crud.customers.list",
51
+ view: "crud.customers.view",
52
+ create: "crud.customers.create",
53
+ update: "crud.customers.update",
54
+ delete: "crud.customers.delete"
55
+ }
56
+ });
57
+
58
+ const workspaceRouteBase = resolveApiBasePath({
59
+ surfaceRequiresWorkspace: true,
60
+ relativePath: "/customers"
61
+ });
62
+ const createRoute = findRoute(registeredRoutes, "POST", workspaceRouteBase);
63
+ const updateRoute = findRoute(registeredRoutes, "PATCH", `${workspaceRouteBase}/:recordId`);
64
+ assert.ok(createRoute);
65
+ assert.ok(updateRoute);
66
+
67
+ const calls = [];
68
+ const executeAction = async (payload) => {
69
+ calls.push(payload);
70
+ return {};
71
+ };
72
+
73
+ await createRoute.handler(
74
+ {
75
+ input: {
76
+ params: { workspaceSlug: "acme" },
77
+ body: { textField: "A", dateField: "2026-03-11", numberField: 2 }
78
+ },
79
+ executeAction
80
+ },
81
+ createReplyDouble()
82
+ );
83
+ await updateRoute.handler(
84
+ {
85
+ input: {
86
+ params: { workspaceSlug: "acme", recordId: 12 },
87
+ body: { textField: "Renamed" }
88
+ },
89
+ executeAction
90
+ },
91
+ createReplyDouble()
92
+ );
93
+
94
+ assert.deepEqual(calls[0].input, {
95
+ workspaceSlug: "acme",
96
+ payload: { textField: "A", dateField: "2026-03-11", numberField: 2 }
97
+ });
98
+ assert.deepEqual(calls[1].input, {
99
+ workspaceSlug: "acme",
100
+ recordId: 12,
101
+ patch: { textField: "Renamed" }
102
+ });
103
+ });
104
+
105
+ test("crud routes omit workspaceSlug for non-workspace calls and apply configured route surface", async () => {
106
+ const registeredRoutes = [];
107
+ const router = {
108
+ register(method, path, route, handler) {
109
+ registeredRoutes.push({
110
+ method,
111
+ path,
112
+ route,
113
+ handler
114
+ });
115
+ }
116
+ };
117
+ const app = {
118
+ make(token) {
119
+ if (token !== "jskit.http.router") {
120
+ throw new Error(`Unexpected token: ${String(token)}`);
121
+ }
122
+ return router;
123
+ }
124
+ };
125
+
126
+ registerRoutes(app, {
127
+ routeRelativePath: "/customers",
128
+ routeSurface: "console",
129
+ actionIds: {
130
+ list: "crud.customers.list",
131
+ view: "crud.customers.view",
132
+ create: "crud.customers.create",
133
+ update: "crud.customers.update",
134
+ delete: "crud.customers.delete"
135
+ }
136
+ });
137
+
138
+ const createRoute = findRoute(registeredRoutes, "POST", "/api/customers");
139
+ assert.ok(createRoute);
140
+ assert.equal(createRoute.route.surface, "console");
141
+
142
+ const calls = [];
143
+ const executeAction = async (payload) => {
144
+ calls.push(payload);
145
+ return {};
146
+ };
147
+
148
+ await createRoute.handler(
149
+ {
150
+ input: {
151
+ params: {},
152
+ body: { textField: "A", dateField: "2026-03-11", numberField: 2 }
153
+ },
154
+ executeAction
155
+ },
156
+ createReplyDouble()
157
+ );
158
+
159
+ assert.deepEqual(calls[0].input, {
160
+ payload: { textField: "A", dateField: "2026-03-11", numberField: 2 }
161
+ });
162
+ });
163
+
164
+ test("crud routes normalize route ownership filter values before registering visibility", () => {
165
+ const registeredRoutes = [];
166
+ const router = {
167
+ register(method, path, route, handler) {
168
+ registeredRoutes.push({
169
+ method,
170
+ path,
171
+ route,
172
+ handler
173
+ });
174
+ }
175
+ };
176
+ const app = {
177
+ make(token) {
178
+ if (token !== "jskit.http.router") {
179
+ throw new Error(`Unexpected token: ${String(token)}`);
180
+ }
181
+ return router;
182
+ }
183
+ };
184
+
185
+ registerRoutes(app, {
186
+ routeRelativePath: "/customers",
187
+ routeOwnershipFilter: " Workspace_User ",
188
+ actionIds: {
189
+ list: "crud.customers.list",
190
+ view: "crud.customers.view",
191
+ create: "crud.customers.create",
192
+ update: "crud.customers.update",
193
+ delete: "crud.customers.delete"
194
+ }
195
+ });
196
+ registerRoutes(app, {
197
+ routeRelativePath: "/customers-public",
198
+ routeOwnershipFilter: "not-a-real-filter",
199
+ actionIds: {
200
+ list: "crud.customers-public.list",
201
+ view: "crud.customers-public.view",
202
+ create: "crud.customers-public.create",
203
+ update: "crud.customers-public.update",
204
+ delete: "crud.customers-public.delete"
205
+ }
206
+ });
207
+
208
+ const workspaceUserRoute = findRoute(registeredRoutes, "GET", "/api/customers");
209
+ const fallbackPublicRoute = findRoute(registeredRoutes, "GET", "/api/customers-public");
210
+
211
+ assert.ok(workspaceUserRoute);
212
+ assert.ok(fallbackPublicRoute);
213
+ assert.equal(workspaceUserRoute.route.visibility, "workspace_user");
214
+ assert.equal(fallbackPublicRoute.route.visibility, "public");
215
+ });