@jskit-ai/crud 0.1.4

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.
Files changed (40) hide show
  1. package/package.descriptor.mjs +322 -0
  2. package/package.json +22 -0
  3. package/src/client/index.js +3 -0
  4. package/src/server/CrudServiceProvider.js +11 -0
  5. package/src/server/actionIds.js +22 -0
  6. package/src/server/actions.js +152 -0
  7. package/src/server/registerRoutes.js +235 -0
  8. package/src/server/repository.js +162 -0
  9. package/src/server/service.js +96 -0
  10. package/src/shared/crud/crudModuleConfig.js +310 -0
  11. package/src/shared/crud/crudResource.js +191 -0
  12. package/src/shared/index.js +12 -0
  13. package/templates/migrations/crud_initial.cjs +42 -0
  14. package/templates/src/elements/CreateElement.vue +115 -0
  15. package/templates/src/elements/EditElement.vue +140 -0
  16. package/templates/src/elements/ListElement.vue +88 -0
  17. package/templates/src/elements/ViewElement.vue +126 -0
  18. package/templates/src/elements/clientSupport.js +41 -0
  19. package/templates/src/local-package/client/index.js +4 -0
  20. package/templates/src/local-package/package.descriptor.mjs +83 -0
  21. package/templates/src/local-package/package.json +14 -0
  22. package/templates/src/local-package/server/CrudServiceProvider.js +87 -0
  23. package/templates/src/local-package/server/actionIds.js +9 -0
  24. package/templates/src/local-package/server/actions.js +151 -0
  25. package/templates/src/local-package/server/diTokens.js +4 -0
  26. package/templates/src/local-package/server/registerRoutes.js +196 -0
  27. package/templates/src/local-package/server/repository.js +1 -0
  28. package/templates/src/local-package/server/service.js +96 -0
  29. package/templates/src/local-package/shared/crudResource.js +1 -0
  30. package/templates/src/local-package/shared/index.js +8 -0
  31. package/templates/src/local-package/shared/moduleConfig.js +169 -0
  32. package/templates/src/pages/admin/crud/[recordId]/edit.vue +7 -0
  33. package/templates/src/pages/admin/crud/[recordId]/index.vue +7 -0
  34. package/templates/src/pages/admin/crud/index.vue +7 -0
  35. package/templates/src/pages/admin/crud/new.vue +7 -0
  36. package/test/crudModuleConfig.test.js +225 -0
  37. package/test/crudResource.test.js +41 -0
  38. package/test/crudServerGuards.test.js +61 -0
  39. package/test/crudService.test.js +83 -0
  40. package/test/routeInputContracts.test.js +211 -0
@@ -0,0 +1,322 @@
1
+ export default Object.freeze({
2
+ packageVersion: 1,
3
+ packageId: "@jskit-ai/crud",
4
+ version: "0.1.4",
5
+ installationMode: "clone-only",
6
+ description: "CRUD module with server routes, actions, persistence, and client pages.",
7
+ options: {
8
+ namespace: {
9
+ required: true,
10
+ inputType: "text",
11
+ defaultValue: "",
12
+ promptLabel: "CRUD namespace",
13
+ promptHint: "Required slug (example: customers, appointments, vendors)."
14
+ },
15
+ surface: {
16
+ required: true,
17
+ inputType: "text",
18
+ defaultFromConfig: "surfaceDefaultId",
19
+ promptLabel: "Target surface",
20
+ promptHint: "Defaults to config.public.surfaceDefaultId. Must match an enabled surface id."
21
+ },
22
+ "ownership-filter": {
23
+ required: true,
24
+ inputType: "text",
25
+ defaultValue: "auto",
26
+ promptLabel: "Ownership filter",
27
+ promptHint: "auto | public | user | workspace | workspace_user"
28
+ },
29
+ "directory-prefix": {
30
+ required: false,
31
+ inputType: "text",
32
+ defaultValue: "",
33
+ promptLabel: "Page directory prefix",
34
+ promptHint: "Optional subpath under the selected surface pages root (example: crm or ops/team-a)."
35
+ }
36
+ },
37
+ optionPolicies: {
38
+ surfaceVisibility: {
39
+ visibilityOption: "ownership-filter"
40
+ }
41
+ },
42
+ dependsOn: [
43
+ "@jskit-ai/auth-core",
44
+ "@jskit-ai/crud-core",
45
+ "@jskit-ai/database-runtime",
46
+ "@jskit-ai/http-runtime",
47
+ "@jskit-ai/realtime",
48
+ "@jskit-ai/shell-web",
49
+ "@jskit-ai/users-core",
50
+ "@jskit-ai/users-web"
51
+ ],
52
+ capabilities: {
53
+ provides: ["crud"],
54
+ requires: [
55
+ "runtime.actions",
56
+ "runtime.database",
57
+ "auth.policy",
58
+ "users.core",
59
+ "users.web",
60
+ "runtime.web-placement",
61
+ "runtime.realtime.client"
62
+ ]
63
+ },
64
+ runtime: {
65
+ server: {
66
+ providers: [
67
+ {
68
+ entrypoint: "src/server/CrudServiceProvider.js",
69
+ export: "CrudServiceProvider"
70
+ }
71
+ ]
72
+ },
73
+ client: {
74
+ providers: []
75
+ }
76
+ },
77
+ metadata: {
78
+ apiSummary: {
79
+ surfaces: [
80
+ {
81
+ subpath: "./server",
82
+ summary: "Scaffold package runtime provider (no-op) plus reference CRUD server modules."
83
+ },
84
+ {
85
+ subpath: "./shared",
86
+ summary: "Exports shared CRUD resource and module config helpers."
87
+ }
88
+ ],
89
+ containerTokens: {
90
+ server: ["crud.<namespace>.repository", "crud.<namespace>.service"],
91
+ client: []
92
+ }
93
+ }
94
+ },
95
+ mutations: {
96
+ dependencies: {
97
+ runtime: {
98
+ "@jskit-ai/auth-core": "0.1.4",
99
+ "@jskit-ai/crud-core": "0.1.4",
100
+ "@jskit-ai/database-runtime": "0.1.4",
101
+ "@jskit-ai/http-runtime": "0.1.4",
102
+ "@jskit-ai/kernel": "0.1.4",
103
+ "@jskit-ai/realtime": "0.1.4",
104
+ "@jskit-ai/shell-web": "0.1.4",
105
+ "@jskit-ai/users-core": "0.1.4",
106
+ "@jskit-ai/users-web": "0.1.4",
107
+ "@local/${option:namespace|kebab}": "file:packages/${option:namespace|kebab}",
108
+ "@tanstack/vue-query": "^5.90.5",
109
+ "typebox": "^1.0.81",
110
+ "vuetify": "^4.0.0"
111
+ },
112
+ dev: {}
113
+ },
114
+ packageJson: {
115
+ scripts: {}
116
+ },
117
+ procfile: {},
118
+ files: [
119
+ {
120
+ op: "install-migration",
121
+ from: "templates/migrations/crud_initial.cjs",
122
+ toDir: "migrations",
123
+ slug: "crud_initial_${option:namespace|snake}",
124
+ extension: ".cjs",
125
+ reason: "Install CRUD schema migration.",
126
+ category: "crud",
127
+ id: "crud-initial-schema-${option:namespace|snake}"
128
+ },
129
+ {
130
+ from: "templates/src/local-package/package.json",
131
+ to: "packages/${option:namespace|kebab}/package.json",
132
+ reason: "Install app-local CRUD package manifest.",
133
+ category: "crud",
134
+ id: "crud-local-package-json-${option:namespace|snake}"
135
+ },
136
+ {
137
+ from: "templates/src/local-package/package.descriptor.mjs",
138
+ to: "packages/${option:namespace|kebab}/package.descriptor.mjs",
139
+ reason: "Install app-local CRUD package descriptor.",
140
+ category: "crud",
141
+ id: "crud-local-package-descriptor-${option:namespace|snake}"
142
+ },
143
+ {
144
+ from: "templates/src/local-package/client/index.js",
145
+ to: "packages/${option:namespace|kebab}/src/client/index.js",
146
+ reason: "Install app-local CRUD client package exports.",
147
+ category: "crud",
148
+ id: "crud-local-package-client-index-${option:namespace|snake}"
149
+ },
150
+ {
151
+ from: "templates/src/local-package/server/diTokens.js",
152
+ to: "packages/${option:namespace|kebab}/src/server/diTokens.js",
153
+ reason: "Install app-local CRUD server DI token constants.",
154
+ category: "crud",
155
+ id: "crud-local-package-server-di-tokens-${option:namespace|snake}"
156
+ },
157
+ {
158
+ from: "templates/src/local-package/server/CrudServiceProvider.js",
159
+ to: "packages/${option:namespace|kebab}/src/server/${option:namespace|pascal}ServiceProvider.js",
160
+ reason: "Install app-local CRUD server provider.",
161
+ category: "crud",
162
+ id: "crud-local-package-server-provider-${option:namespace|snake}"
163
+ },
164
+ {
165
+ from: "templates/src/local-package/server/actions.js",
166
+ to: "packages/${option:namespace|kebab}/src/server/actions.js",
167
+ reason: "Install app-local CRUD action definitions.",
168
+ category: "crud",
169
+ id: "crud-local-package-server-actions-${option:namespace|snake}"
170
+ },
171
+ {
172
+ from: "templates/src/local-package/server/actionIds.js",
173
+ to: "packages/${option:namespace|kebab}/src/server/actionIds.js",
174
+ reason: "Install app-local CRUD action IDs.",
175
+ category: "crud",
176
+ id: "crud-local-package-server-action-ids-${option:namespace|snake}"
177
+ },
178
+ {
179
+ from: "templates/src/local-package/server/registerRoutes.js",
180
+ to: "packages/${option:namespace|kebab}/src/server/registerRoutes.js",
181
+ reason: "Install app-local CRUD route registration.",
182
+ category: "crud",
183
+ id: "crud-local-package-server-routes-${option:namespace|snake}"
184
+ },
185
+ {
186
+ from: "src/server/repository.js",
187
+ to: "packages/${option:namespace|kebab}/src/server/repository.js",
188
+ reason: "Install app-local CRUD repository.",
189
+ category: "crud",
190
+ id: "crud-local-package-server-repository-${option:namespace|snake}"
191
+ },
192
+ {
193
+ from: "templates/src/local-package/server/service.js",
194
+ to: "packages/${option:namespace|kebab}/src/server/service.js",
195
+ reason: "Install app-local CRUD service.",
196
+ category: "crud",
197
+ id: "crud-local-package-server-service-${option:namespace|snake}"
198
+ },
199
+ {
200
+ from: "templates/src/local-package/shared/index.js",
201
+ to: "packages/${option:namespace|kebab}/src/shared/index.js",
202
+ reason: "Install app-local CRUD shared exports.",
203
+ category: "crud",
204
+ id: "crud-local-package-shared-index-${option:namespace|snake}"
205
+ },
206
+ {
207
+ from: "templates/src/local-package/shared/moduleConfig.js",
208
+ to: "packages/${option:namespace|kebab}/src/shared/moduleConfig.js",
209
+ reason: "Install app-local CRUD shared module config.",
210
+ category: "crud",
211
+ id: "crud-local-package-shared-module-config-${option:namespace|snake}"
212
+ },
213
+ {
214
+ from: "src/shared/crud/crudResource.js",
215
+ to: "packages/${option:namespace|kebab}/src/shared/${option:namespace|singular|camel}Resource.js",
216
+ reason: "Install app-local CRUD resource.",
217
+ category: "crud",
218
+ id: "crud-local-package-shared-resource-${option:namespace|snake}"
219
+ },
220
+ {
221
+ from: "templates/src/elements/clientSupport.js",
222
+ to: "packages/${option:namespace|kebab}/src/client/clientSupport.js",
223
+ reason: "Install app-local CRUD client support helpers.",
224
+ category: "crud",
225
+ id: "crud-local-package-client-support-${option:namespace|snake}"
226
+ },
227
+ {
228
+ from: "templates/src/elements/ListElement.vue",
229
+ to: "packages/${option:namespace|kebab}/src/client/List${option:namespace|plural|pascal}Element.vue",
230
+ reason: "Install app-local CRUD list element.",
231
+ category: "crud",
232
+ id: "crud-local-package-client-list-${option:namespace|snake}"
233
+ },
234
+ {
235
+ from: "templates/src/elements/ViewElement.vue",
236
+ to: "packages/${option:namespace|kebab}/src/client/View${option:namespace|singular|pascal}Element.vue",
237
+ reason: "Install app-local CRUD view element.",
238
+ category: "crud",
239
+ id: "crud-local-package-client-view-${option:namespace|snake}"
240
+ },
241
+ {
242
+ from: "templates/src/elements/CreateElement.vue",
243
+ to: "packages/${option:namespace|kebab}/src/client/Create${option:namespace|singular|pascal}Element.vue",
244
+ reason: "Install app-local CRUD create element.",
245
+ category: "crud",
246
+ id: "crud-local-package-client-create-${option:namespace|snake}"
247
+ },
248
+ {
249
+ from: "templates/src/elements/EditElement.vue",
250
+ to: "packages/${option:namespace|kebab}/src/client/Edit${option:namespace|singular|pascal}Element.vue",
251
+ reason: "Install app-local CRUD edit element.",
252
+ category: "crud",
253
+ id: "crud-local-package-client-edit-${option:namespace|snake}"
254
+ },
255
+ {
256
+ from: "templates/src/pages/admin/crud/index.vue",
257
+ toSurface: "${option:surface|lower}",
258
+ toSurfacePath: "${option:directory-prefix|pathprefix}${option:namespace|kebab}/index.vue",
259
+ reason: "Install CRUD list page scaffold.",
260
+ category: "crud",
261
+ id: "crud-page-surface-crud-index",
262
+ when: {
263
+ config: "tenancyMode",
264
+ in: ["personal", "workspace"]
265
+ }
266
+ },
267
+ {
268
+ from: "templates/src/pages/admin/crud/new.vue",
269
+ toSurface: "${option:surface|lower}",
270
+ toSurfacePath: "${option:directory-prefix|pathprefix}${option:namespace|kebab}/new.vue",
271
+ reason: "Install CRUD create page scaffold.",
272
+ category: "crud",
273
+ id: "crud-page-surface-crud-new",
274
+ when: {
275
+ config: "tenancyMode",
276
+ in: ["personal", "workspace"]
277
+ }
278
+ },
279
+ {
280
+ from: "templates/src/pages/admin/crud/[recordId]/index.vue",
281
+ toSurface: "${option:surface|lower}",
282
+ toSurfacePath: "${option:directory-prefix|pathprefix}${option:namespace|kebab}/[recordId]/index.vue",
283
+ reason: "Install CRUD detail page scaffold.",
284
+ category: "crud",
285
+ id: "crud-page-surface-crud-view",
286
+ when: {
287
+ config: "tenancyMode",
288
+ in: ["personal", "workspace"]
289
+ }
290
+ },
291
+ {
292
+ from: "templates/src/pages/admin/crud/[recordId]/edit.vue",
293
+ toSurface: "${option:surface|lower}",
294
+ toSurfacePath: "${option:directory-prefix|pathprefix}${option:namespace|kebab}/[recordId]/edit.vue",
295
+ reason: "Install CRUD edit page scaffold.",
296
+ category: "crud",
297
+ id: "crud-page-surface-crud-edit",
298
+ when: {
299
+ config: "tenancyMode",
300
+ in: ["personal", "workspace"]
301
+ }
302
+ }
303
+ ],
304
+ text: [
305
+ {
306
+ op: "append-text",
307
+ file: "src/placement.js",
308
+ position: "bottom",
309
+ skipIfContains: "jskit:crud.menu:${option:namespace|kebab}:${option:directory-prefix|path}",
310
+ value:
311
+ "\n// jskit:crud.menu:${option:namespace|kebab}:${option:directory-prefix|path}\nimport { crudModuleConfig as crud${option:namespace|pascal}ModuleConfig } from \"@local/${option:namespace|kebab}/shared\";\n{\n const crudNamespace = \"${option:namespace|kebab}\";\n\n addPlacement({\n id: \"crud.\" + crudNamespace + \".menu\",\n host: \"shell-layout\",\n position: \"primary-menu\",\n surfaces: [\"${option:surface|lower}\"],\n order: 150,\n componentToken: \"users.web.shell.surface-aware-menu-link-item\",\n props: {\n label: \"${option:namespace|plural|pascal}\",\n surface: \"${option:surface|lower}\",\n workspaceSuffix: crud${option:namespace|pascal}ModuleConfig.relativePath,\n nonWorkspaceSuffix: crud${option:namespace|pascal}ModuleConfig.relativePath\n },\n when: ({ auth }) => Boolean(auth?.authenticated)\n });\n}\n",
312
+ reason: "Append CRUD menu placement into app-owned placement registry.",
313
+ category: "crud",
314
+ id: "crud-placement-menu",
315
+ when: {
316
+ config: "tenancyMode",
317
+ in: ["personal", "workspace"]
318
+ }
319
+ }
320
+ ]
321
+ }
322
+ });
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@jskit-ai/crud",
3
+ "version": "0.1.4",
4
+ "type": "module",
5
+ "scripts": {
6
+ "test": "node --test"
7
+ },
8
+ "exports": {
9
+ "./client": "./src/client/index.js",
10
+ "./server/CrudServiceProvider": "./src/server/CrudServiceProvider.js",
11
+ "./shared": "./src/shared/index.js",
12
+ "./shared/crud/*": "./src/shared/crud/*.js"
13
+ },
14
+ "dependencies": {
15
+ "@jskit-ai/crud-core": "0.1.4",
16
+ "@jskit-ai/database-runtime": "0.1.4",
17
+ "@jskit-ai/http-runtime": "0.1.4",
18
+ "@jskit-ai/kernel": "0.1.4",
19
+ "@jskit-ai/users-core": "0.1.4",
20
+ "typebox": "^1.0.81"
21
+ }
22
+ }
@@ -0,0 +1,3 @@
1
+ const clientProviders = Object.freeze([]);
2
+
3
+ export { clientProviders };
@@ -0,0 +1,11 @@
1
+ class CrudServiceProvider {
2
+ static id = "crud";
3
+
4
+ static dependsOn = [];
5
+
6
+ register() {}
7
+
8
+ boot() {}
9
+ }
10
+
11
+ export { CrudServiceProvider };
@@ -0,0 +1,22 @@
1
+ function requireActionIdPrefix(actionIdPrefix) {
2
+ const prefix = String(actionIdPrefix || "").trim();
3
+ if (!prefix) {
4
+ throw new TypeError("createActionIds requires actionIdPrefix.");
5
+ }
6
+
7
+ return prefix;
8
+ }
9
+
10
+ function createActionIds(actionIdPrefix) {
11
+ const prefix = requireActionIdPrefix(actionIdPrefix);
12
+
13
+ return Object.freeze({
14
+ list: `${prefix}.list`,
15
+ view: `${prefix}.view`,
16
+ create: `${prefix}.create`,
17
+ update: `${prefix}.update`,
18
+ delete: `${prefix}.delete`
19
+ });
20
+ }
21
+
22
+ export { requireActionIdPrefix, createActionIds };
@@ -0,0 +1,152 @@
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 { crudResource } from "../shared/crud/crudResource.js";
7
+ import { createActionIds } 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({ actionIdPrefix, surface = "" } = {}) {
19
+ const actionIds = createActionIds(actionIdPrefix);
20
+ const actionSurface = requireActionSurface(surface);
21
+
22
+ return Object.freeze([
23
+ {
24
+ id: actionIds.list,
25
+ version: 1,
26
+ kind: "query",
27
+ channels: ["api", "automation", "internal"],
28
+ surfaces: [actionSurface],
29
+ permission: {
30
+ require: "authenticated"
31
+ },
32
+ inputValidator: [workspaceSlugParamsValidator, cursorPaginationQueryValidator],
33
+ outputValidator: crudResource.operations.list.outputValidator,
34
+ idempotency: "none",
35
+ audit: {
36
+ actionName: actionIds.list
37
+ },
38
+ observability: {},
39
+ async execute(input, context, deps) {
40
+ return deps.crudService.listRecords(input, {
41
+ context,
42
+ visibilityContext: context?.visibilityContext
43
+ });
44
+ }
45
+ },
46
+ {
47
+ id: actionIds.view,
48
+ version: 1,
49
+ kind: "query",
50
+ channels: ["api", "automation", "internal"],
51
+ surfaces: [actionSurface],
52
+ permission: {
53
+ require: "authenticated"
54
+ },
55
+ inputValidator: [workspaceSlugParamsValidator, recordIdParamsValidator],
56
+ outputValidator: crudResource.operations.view.outputValidator,
57
+ idempotency: "none",
58
+ audit: {
59
+ actionName: actionIds.view
60
+ },
61
+ observability: {},
62
+ async execute(input, context, deps) {
63
+ return deps.crudService.getRecord(input.recordId, {
64
+ context,
65
+ visibilityContext: context?.visibilityContext
66
+ });
67
+ }
68
+ },
69
+ {
70
+ id: actionIds.create,
71
+ version: 1,
72
+ kind: "command",
73
+ channels: ["api", "automation", "internal"],
74
+ surfaces: [actionSurface],
75
+ permission: {
76
+ require: "authenticated"
77
+ },
78
+ inputValidator: [
79
+ workspaceSlugParamsValidator,
80
+ {
81
+ payload: crudResource.operations.create.bodyValidator
82
+ }
83
+ ],
84
+ outputValidator: crudResource.operations.create.outputValidator,
85
+ idempotency: "optional",
86
+ audit: {
87
+ actionName: actionIds.create
88
+ },
89
+ observability: {},
90
+ async execute(input, context, deps) {
91
+ return deps.crudService.createRecord(input.payload, {
92
+ context,
93
+ visibilityContext: context?.visibilityContext
94
+ });
95
+ }
96
+ },
97
+ {
98
+ id: actionIds.update,
99
+ version: 1,
100
+ kind: "command",
101
+ channels: ["api", "automation", "internal"],
102
+ surfaces: [actionSurface],
103
+ permission: {
104
+ require: "authenticated"
105
+ },
106
+ inputValidator: [
107
+ workspaceSlugParamsValidator,
108
+ recordIdParamsValidator,
109
+ {
110
+ patch: crudResource.operations.patch.bodyValidator
111
+ }
112
+ ],
113
+ outputValidator: crudResource.operations.patch.outputValidator,
114
+ idempotency: "optional",
115
+ audit: {
116
+ actionName: actionIds.update
117
+ },
118
+ observability: {},
119
+ async execute(input, context, deps) {
120
+ return deps.crudService.updateRecord(input.recordId, input.patch, {
121
+ context,
122
+ visibilityContext: context?.visibilityContext
123
+ });
124
+ }
125
+ },
126
+ {
127
+ id: actionIds.delete,
128
+ version: 1,
129
+ kind: "command",
130
+ channels: ["api", "automation", "internal"],
131
+ surfaces: [actionSurface],
132
+ permission: {
133
+ require: "authenticated"
134
+ },
135
+ inputValidator: [workspaceSlugParamsValidator, recordIdParamsValidator],
136
+ outputValidator: crudResource.operations.delete.outputValidator,
137
+ idempotency: "optional",
138
+ audit: {
139
+ actionName: actionIds.delete
140
+ },
141
+ observability: {},
142
+ async execute(input, context, deps) {
143
+ return deps.crudService.deleteRecord(input.recordId, {
144
+ context,
145
+ visibilityContext: context?.visibilityContext
146
+ });
147
+ }
148
+ }
149
+ ]);
150
+ }
151
+
152
+ export { createActions };