@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,143 +1,90 @@
1
- import { Type } from "typebox";
2
- import {
3
- toIsoString
4
- } from "@jskit-ai/database-runtime/shared";
5
- import {
6
- normalizeObjectInput,
7
- createCursorListValidator,
8
- recordIdSchema
9
- } from "@jskit-ai/kernel/shared/validators";
10
- import {
11
- normalizeText,
12
- normalizeRecordId,
13
- normalizeFiniteNumber,
14
- normalizeIfPresent
15
- } from "@jskit-ai/kernel/shared/support/normalize";
1
+ import { defineCrudResource } from "@jskit-ai/resource-crud-core/shared/crudResource";
16
2
 
17
- const RESOURCE_LOOKUP_CONTAINER_KEY = "lookups";
18
-
19
- const recordOutputSchema = Type.Object(
20
- {
21
- id: recordIdSchema,
22
- textField: Type.String({ minLength: 1, maxLength: 160 }),
23
- dateField: Type.String({ minLength: 1 }),
24
- numberField: Type.Number(),
25
- createdAt: Type.String({ minLength: 1 }),
26
- updatedAt: Type.String({ minLength: 1 }),
27
- [RESOURCE_LOOKUP_CONTAINER_KEY]: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
28
- },
29
- { additionalProperties: false }
30
- );
31
-
32
- const recordBodySchema = Type.Object(
33
- {
34
- textField: Type.String({
3
+ const crudResource = defineCrudResource({
4
+ namespace: "crud",
5
+ tableName: "crud",
6
+ schema: {
7
+ textField: {
8
+ type: "string",
9
+ required: true,
35
10
  minLength: 1,
36
11
  maxLength: 160,
37
- messages: {
38
- required: "Text field is required.",
39
- minLength: "Text field is required.",
40
- maxLength: "Text field must be at most 160 characters.",
41
- default: "Text field is required."
12
+ operations: {
13
+ output: { required: true },
14
+ create: {
15
+ required: true,
16
+ messages: {
17
+ required: "Text field is required.",
18
+ minLength: "Text field is required.",
19
+ maxLength: "Text field must be at most 160 characters.",
20
+ default: "Text field is required."
21
+ }
22
+ },
23
+ patch: {
24
+ required: false,
25
+ messages: {
26
+ minLength: "Text field is required.",
27
+ maxLength: "Text field must be at most 160 characters.",
28
+ default: "Text field is required."
29
+ }
30
+ }
42
31
  }
43
- }),
44
- dateField: Type.String({
45
- minLength: 1,
46
- messages: {
47
- required: "Date field is required.",
48
- minLength: "Date field is required.",
49
- default: "Date field is required."
32
+ },
33
+ dateField: {
34
+ type: "dateTime",
35
+ required: true,
36
+ operations: {
37
+ output: { required: true },
38
+ create: {
39
+ required: true,
40
+ messages: {
41
+ required: "Date field is required.",
42
+ default: "Date field is required."
43
+ }
44
+ },
45
+ patch: {
46
+ required: false,
47
+ messages: {
48
+ default: "Date field is required."
49
+ }
50
+ }
50
51
  }
51
- }),
52
- numberField: Type.Number({
53
- messages: {
54
- required: "Number field is required.",
55
- default: "Number field must be a valid number."
52
+ },
53
+ numberField: {
54
+ type: "number",
55
+ required: true,
56
+ operations: {
57
+ output: { required: true },
58
+ create: {
59
+ required: true,
60
+ messages: {
61
+ required: "Number field is required.",
62
+ default: "Number field must be a valid number."
63
+ }
64
+ },
65
+ patch: {
66
+ required: false,
67
+ messages: {
68
+ default: "Number field must be a valid number."
69
+ }
70
+ }
56
71
  }
57
- })
58
- },
59
- {
60
- additionalProperties: false,
61
- messages: {
62
- additionalProperties: "Unexpected field.",
63
- default: "Invalid value."
64
- }
65
- }
66
- );
67
-
68
- const patchBodySchema = Type.Partial(recordBodySchema, { additionalProperties: false });
69
-
70
- const recordOutputValidator = Object.freeze({
71
- schema: recordOutputSchema,
72
- normalize(payload = {}) {
73
- const source = normalizeObjectInput(payload);
74
- const normalized = {
75
- id: normalizeRecordId(source.id, { fallback: "" }),
76
- textField: normalizeText(source.textField),
77
- dateField: toIsoString(source.dateField),
78
- numberField: normalizeFiniteNumber(source.numberField),
79
- createdAt: normalizeIfPresent(source.createdAt, toIsoString),
80
- updatedAt: normalizeIfPresent(source.updatedAt, toIsoString)
81
- };
82
- if (Object.hasOwn(source, RESOURCE_LOOKUP_CONTAINER_KEY)) {
83
- normalized[RESOURCE_LOOKUP_CONTAINER_KEY] = source[RESOURCE_LOOKUP_CONTAINER_KEY];
84
- }
85
-
86
- return normalized;
87
- }
88
- });
89
-
90
- const listOutputValidator = createCursorListValidator(recordOutputValidator);
91
-
92
- const createBodyValidator = Object.freeze({
93
- schema: recordBodySchema,
94
- normalize(payload = {}) {
95
- const source = normalizeObjectInput(payload);
96
- const normalized = {};
97
-
98
- if (Object.hasOwn(source, "textField")) {
99
- normalized.textField = normalizeText(source.textField);
100
- }
101
- if (Object.hasOwn(source, "dateField")) {
102
- normalized.dateField = toIsoString(source.dateField);
103
- }
104
- if (Object.hasOwn(source, "numberField")) {
105
- normalized.numberField = normalizeFiniteNumber(source.numberField);
106
- }
107
-
108
- return normalized;
109
- }
110
- });
111
-
112
- const patchBodyValidator = Object.freeze({
113
- schema: patchBodySchema,
114
- normalize: createBodyValidator.normalize
115
- });
116
-
117
- const deleteOutputValidator = Object.freeze({
118
- schema: Type.Object(
119
- {
120
- id: recordIdSchema,
121
- deleted: Type.Literal(true)
122
72
  },
123
- { additionalProperties: false }
124
- ),
125
- normalize(payload = {}) {
126
- const source = normalizeObjectInput(payload);
127
-
128
- return {
129
- id: normalizeRecordId(source.id, { fallback: "" }),
130
- deleted: true
131
- };
132
- }
133
- });
134
-
135
- const CRUD_RESOURCE_FIELD_META = [];
136
-
137
- const crudResource = {
138
- namespace: "crud",
139
- tableName: "crud",
140
- idColumn: "id",
73
+ createdAt: {
74
+ type: "dateTime",
75
+ required: true,
76
+ operations: {
77
+ output: { required: true }
78
+ }
79
+ },
80
+ updatedAt: {
81
+ type: "dateTime",
82
+ required: true,
83
+ operations: {
84
+ output: { required: true }
85
+ }
86
+ }
87
+ },
141
88
  messages: {
142
89
  validation: "Fix invalid CRUD values and try again.",
143
90
  saveSuccess: "Record saved.",
@@ -147,58 +94,11 @@ const crudResource = {
147
94
  },
148
95
  contract: {
149
96
  lookup: {
150
- containerKey: RESOURCE_LOOKUP_CONTAINER_KEY,
151
- defaultInclude: "*", // Set "none" to disable lookup hydration unless include=... is passed.
152
- maxDepth: 3 // Lower this to limit nested lookup hydration depth.
97
+ containerKey: "lookups",
98
+ defaultInclude: "*",
99
+ maxDepth: 3
153
100
  }
154
- },
155
- operations: {
156
- list: {
157
- realtime: {
158
- events: ["crud.record.changed"] // Add more events e.g. for lookup records
159
- },
160
- method: "GET",
161
- outputValidator: listOutputValidator
162
- },
163
- view: {
164
- method: "GET",
165
- outputValidator: recordOutputValidator
166
- },
167
- create: {
168
- method: "POST",
169
- bodyValidator: createBodyValidator,
170
- outputValidator: recordOutputValidator
171
- },
172
- patch: {
173
- method: "PATCH",
174
- bodyValidator: patchBodyValidator,
175
- outputValidator: recordOutputValidator
176
- },
177
- delete: {
178
- method: "DELETE",
179
- outputValidator: deleteOutputValidator
180
- }
181
- },
182
- fieldMeta: CRUD_RESOURCE_FIELD_META
183
- };
184
-
185
- void CRUD_RESOURCE_FIELD_META;
186
-
187
- // Example 1:n collection hydration:
188
- // CRUD_RESOURCE_FIELD_META.push({
189
- // key: "pets",
190
- // relation: {
191
- // kind: "collection",
192
- // namespace: "pets",
193
- // foreignKey: "customerId",
194
- // parentValueKey: "id",
195
- // hydrateOnList: false, // list: opt-in with include=pets
196
- // hydrateOnView: true // view: hydrated by default
197
- // }
198
- // });
199
- //
200
- // To hydrate child lookups too, request nested include paths:
201
- // - include=pets
202
- // - include=pets,pets.breedId
101
+ }
102
+ });
203
103
 
204
104
  export { crudResource };
@@ -9,8 +9,9 @@ export default Object.freeze({
9
9
  "@jskit-ai/crud-core",
10
10
  "@jskit-ai/database-runtime",
11
11
  "@jskit-ai/http-runtime",
12
+ "@jskit-ai/json-rest-api-core",
12
13
  "@jskit-ai/realtime",
13
- "@jskit-ai/users-core"
14
+ "@jskit-ai/resource-crud-core",
14
15
  ],
15
16
  capabilities: {
16
17
  provides: [
@@ -20,7 +21,7 @@ export default Object.freeze({
20
21
  "runtime.actions",
21
22
  "runtime.database",
22
23
  "auth.policy",
23
- "users.core"
24
+ "json-rest-api.core"
24
25
  ]
25
26
  },
26
27
  runtime: {
@@ -36,10 +37,6 @@ export default Object.freeze({
36
37
  metadata: {
37
38
  apiSummary: {
38
39
  surfaces: [
39
- {
40
- subpath: "./server/actionIds",
41
- summary: "App-local CRUD public action identifiers."
42
- },
43
40
  {
44
41
  subpath: "./shared",
45
42
  summary: "App-local CRUD shared resource."
@@ -4,7 +4,6 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
7
- "./server/actionIds": "./src/server/actionIds.js",
8
7
  "./shared": "./src/shared/index.js"
9
8
  }
10
9
  }
@@ -1,23 +1,28 @@
1
1
  import { resolveAppConfig } from "@jskit-ai/kernel/server/support";
2
2
  import { resolveCrudSurfacePolicyFromAppConfig } from "@jskit-ai/crud-core/server/crudModuleConfig";
3
+ import { createCrudJsonApiServiceEvents } from "@jskit-ai/crud-core/server/serviceEvents";
3
4
  import {
4
- createCrudLookupResolver,
5
- createCrudLookup
6
- } from "@jskit-ai/crud-core/server/lookups";
5
+ INTERNAL_JSON_REST_API,
6
+ addResourceIfMissing,
7
+ createJsonRestResourceScopeOptions
8
+ } from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
7
9
  import { withActionDefaults } from "@jskit-ai/kernel/shared/actions";
10
+ import { toDatabaseDateTimeUtc } from "@jskit-ai/database-runtime/shared";
8
11
  import { createRepository } from "./repository.js";
9
- import {
10
- createService,
11
- serviceEvents
12
- } from "./service.js";
12
+ import { createService } from "./service.js";
13
13
  import { createActions } from "./actions.js";
14
14
  import { registerRoutes } from "./registerRoutes.js";
15
+ import { resource } from "../shared/${option:namespace|singular|camel}Resource.js";
15
16
  const CRUD_MODULE_CONFIG = Object.freeze({
16
17
  namespace: "${option:namespace|snake}",
17
18
  surface: __JSKIT_CRUD_SURFACE_ID__,
18
19
  ownershipFilter: "__JSKIT_CRUD_RESOLVED_OWNERSHIP_FILTER__",
19
20
  relativePath: "/${option:directory-prefix|pathprefix}${option:namespace|kebab}"
20
21
  });
22
+ const baseServiceEvents = createCrudJsonApiServiceEvents(CRUD_MODULE_CONFIG.namespace);
23
+ const serviceEvents = {
24
+ ...baseServiceEvents
25
+ };
21
26
 
22
27
  function resolveCrudPolicyFromApp(app) {
23
28
  return resolveCrudSurfacePolicyFromAppConfig(CRUD_MODULE_CONFIG, resolveAppConfig(app), {
@@ -28,25 +33,17 @@ function resolveCrudPolicyFromApp(app) {
28
33
  class ${option:namespace|pascal}Provider {
29
34
  static id = "crud.${option:namespace|snake}";
30
35
 
31
- static dependsOn = ["runtime.actions", "runtime.database", "auth.policy.fastify", "local.main", "users.core"];
36
+ static dependsOn = ["runtime.actions", "runtime.database", "auth.policy.fastify", "local.main", "json-rest-api.core"];
32
37
 
33
38
  register(app) {
34
- if (!app || typeof app.singleton !== "function" || typeof app.service !== "function" || typeof app.actions !== "function") {
35
- throw new Error("${option:namespace|pascal}Provider requires application singleton()/service()/actions().");
36
- }
37
-
38
39
  const crudPolicy = resolveCrudPolicyFromApp(app);
39
40
 
40
41
  app.singleton("repository.${option:namespace|snake}", (scope) => {
42
+ const api = scope.make(INTERNAL_JSON_REST_API);
41
43
  const knex = scope.make("jskit.database.knex");
42
- return createRepository(knex, {
43
- resolveLookup: createCrudLookupResolver(scope)
44
- });
45
- });
46
-
47
- app.singleton("lookup.${option:namespace|snake}", (scope) => {
48
- return createCrudLookup(scope.make("repository.${option:namespace|snake}"), {
49
- ownershipFilter: crudPolicy.ownershipFilter
44
+ return createRepository({
45
+ api,
46
+ knex
50
47
  });
51
48
  });
52
49
 
@@ -77,8 +74,18 @@ class ${option:namespace|pascal}Provider {
77
74
  );
78
75
  }
79
76
 
80
- boot(app) {
77
+ async boot(app) {
81
78
  const crudPolicy = resolveCrudPolicyFromApp(app);
79
+ const api = app.make(INTERNAL_JSON_REST_API);
80
+ await addResourceIfMissing(
81
+ api,
82
+ __JSKIT_CRUD_JSONREST_SCOPE_NAME__,
83
+ createJsonRestResourceScopeOptions(resource, {
84
+ writeSerializers: {
85
+ "datetime-utc": toDatabaseDateTimeUtc
86
+ }
87
+ })
88
+ );
82
89
  registerRoutes(app, {
83
90
  routeOwnershipFilter: crudPolicy.ownershipFilter,
84
91
  routeSurface: crudPolicy.surfaceId,
@@ -1,4 +1,5 @@
1
1
  import {
2
+ composeSchemaDefinitions,
2
3
  recordIdParamsValidator
3
4
  } from "@jskit-ai/kernel/shared/validators";
4
5
  import {
@@ -8,130 +9,117 @@ import {
8
9
  createCrudParentFilterQueryValidator
9
10
  } from "@jskit-ai/crud-core/server/listQueryValidators";
10
11
  import { resource } from "../shared/${option:namespace|singular|camel}Resource.js";
11
- import { actionIds } from "./actionIds.js";
12
- import { LIST_CONFIG } from "./listConfig.js";
13
12
  __JSKIT_CRUD_ACTION_WORKSPACE_VALIDATOR_IMPORT__
14
13
 
15
- const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator(LIST_CONFIG);
14
+ const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator({
15
+ orderBy: resource.defaultSort
16
+ });
16
17
  const listParentFilterQueryValidator = createCrudParentFilterQueryValidator(resource);
17
18
  __JSKIT_CRUD_ACTION_PERMISSION_SUPPORT__
18
19
 
19
- function requireActionSurface(surface = "") {
20
- const normalizedSurface = String(surface || "").trim().toLowerCase();
21
- if (!normalizedSurface) {
22
- throw new TypeError("createActions requires a non-empty surface.");
23
- }
24
-
25
- return normalizedSurface;
26
- }
27
-
28
- function createActions({ surface = "" } = {}) {
29
- const actionSurface = requireActionSurface(surface);
30
-
20
+ function createActions({ surface } = {}) {
31
21
  return Object.freeze([
32
22
  {
33
- id: actionIds.list,
23
+ id: "crud.${option:namespace|snake}.list",
34
24
  version: 1,
35
25
  kind: "query",
36
26
  channels: ["api", "automation", "internal"],
37
- surfaces: [actionSurface],
27
+ surfaces: [surface],
38
28
  permission: __JSKIT_CRUD_LIST_ACTION_PERMISSION__,
39
- inputValidator: __JSKIT_CRUD_LIST_ACTION_INPUT_VALIDATOR__,
40
- outputValidator: resource.operations.list.outputValidator,
29
+ input: __JSKIT_CRUD_LIST_ACTION_INPUT__,
30
+ output: null,
41
31
  idempotency: "none",
42
32
  audit: {
43
- actionName: actionIds.list
33
+ actionName: "crud.${option:namespace|snake}.list"
44
34
  },
45
35
  observability: {},
46
36
  async execute(input, context, deps) {
47
- return deps.${option:namespace|camel}Service.listRecords(input, {
48
- context,
49
- visibilityContext: context?.visibilityContext
37
+ const { workspaceSlug, ...query } = input || {};
38
+ return deps.${option:namespace|camel}Service.queryDocuments(query, {
39
+ context
50
40
  });
51
41
  }
52
42
  },
53
43
  {
54
- id: actionIds.view,
44
+ id: "crud.${option:namespace|snake}.view",
55
45
  version: 1,
56
46
  kind: "query",
57
47
  channels: ["api", "automation", "internal"],
58
- surfaces: [actionSurface],
48
+ surfaces: [surface],
59
49
  permission: __JSKIT_CRUD_VIEW_ACTION_PERMISSION__,
60
- inputValidator: __JSKIT_CRUD_VIEW_ACTION_INPUT_VALIDATOR__,
61
- outputValidator: resource.operations.view.outputValidator,
50
+ input: __JSKIT_CRUD_VIEW_ACTION_INPUT__,
51
+ output: null,
62
52
  idempotency: "none",
63
53
  audit: {
64
- actionName: actionIds.view
54
+ actionName: "crud.${option:namespace|snake}.view"
65
55
  },
66
56
  observability: {},
67
57
  async execute(input, context, deps) {
68
- return deps.${option:namespace|camel}Service.getRecord(input.recordId, {
58
+ return deps.${option:namespace|camel}Service.getDocumentById(input.recordId, {
69
59
  context,
70
- visibilityContext: context?.visibilityContext,
71
60
  include: input.include
72
61
  });
73
62
  }
74
63
  },
75
64
  {
76
- id: actionIds.create,
65
+ id: "crud.${option:namespace|snake}.create",
77
66
  version: 1,
78
67
  kind: "command",
79
68
  channels: ["api", "automation", "internal"],
80
- surfaces: [actionSurface],
69
+ surfaces: [surface],
81
70
  permission: __JSKIT_CRUD_CREATE_ACTION_PERMISSION__,
82
- inputValidator: __JSKIT_CRUD_CREATE_ACTION_INPUT_VALIDATOR__,
83
- outputValidator: resource.operations.create.outputValidator,
71
+ input: __JSKIT_CRUD_CREATE_ACTION_INPUT__,
72
+ output: null,
84
73
  idempotency: "optional",
85
74
  audit: {
86
- actionName: actionIds.create
75
+ actionName: "crud.${option:namespace|snake}.create"
87
76
  },
88
77
  observability: {},
89
78
  async execute(input, context, deps) {
90
- return deps.${option:namespace|camel}Service.createRecord(input.payload, {
91
- context,
92
- visibilityContext: context?.visibilityContext
79
+ const { workspaceSlug, ...payload } = input || {};
80
+ return deps.${option:namespace|camel}Service.createDocument(payload, {
81
+ context
93
82
  });
94
83
  }
95
84
  },
96
85
  {
97
- id: actionIds.update,
86
+ id: "crud.${option:namespace|snake}.update",
98
87
  version: 1,
99
88
  kind: "command",
100
89
  channels: ["api", "automation", "internal"],
101
- surfaces: [actionSurface],
90
+ surfaces: [surface],
102
91
  permission: __JSKIT_CRUD_UPDATE_ACTION_PERMISSION__,
103
- inputValidator: __JSKIT_CRUD_UPDATE_ACTION_INPUT_VALIDATOR__,
104
- outputValidator: resource.operations.patch.outputValidator,
92
+ input: __JSKIT_CRUD_UPDATE_ACTION_INPUT__,
93
+ output: null,
105
94
  idempotency: "optional",
106
95
  audit: {
107
- actionName: actionIds.update
96
+ actionName: "crud.${option:namespace|snake}.update"
108
97
  },
109
98
  observability: {},
110
99
  async execute(input, context, deps) {
111
- return deps.${option:namespace|camel}Service.updateRecord(input.recordId, input.patch, {
112
- context,
113
- visibilityContext: context?.visibilityContext
100
+ const { workspaceSlug, recordId, ...patch } = input || {};
101
+ return deps.${option:namespace|camel}Service.patchDocumentById(recordId, patch, {
102
+ context
114
103
  });
115
104
  }
116
105
  },
117
106
  {
118
- id: actionIds.delete,
107
+ id: "crud.${option:namespace|snake}.delete",
119
108
  version: 1,
120
109
  kind: "command",
121
110
  channels: ["api", "automation", "internal"],
122
- surfaces: [actionSurface],
111
+ surfaces: [surface],
123
112
  permission: __JSKIT_CRUD_DELETE_ACTION_PERMISSION__,
124
- inputValidator: __JSKIT_CRUD_DELETE_ACTION_INPUT_VALIDATOR__,
125
- outputValidator: resource.operations.delete.outputValidator,
113
+ input: __JSKIT_CRUD_DELETE_ACTION_INPUT__,
114
+ output: null,
126
115
  idempotency: "optional",
127
116
  audit: {
128
- actionName: actionIds.delete
117
+ actionName: "crud.${option:namespace|snake}.delete"
129
118
  },
130
119
  observability: {},
131
120
  async execute(input, context, deps) {
132
- return deps.${option:namespace|camel}Service.deleteRecord(input.recordId, {
133
- context,
134
- visibilityContext: context?.visibilityContext
121
+ return deps.${option:namespace|camel}Service.deleteDocumentById(input.recordId, {
122
+ context
135
123
  });
136
124
  }
137
125
  }