@jskit-ai/crud-server-generator 0.1.65 → 0.1.67

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,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/crud-server-generator",
4
- version: "0.1.65",
4
+ version: "0.1.67",
5
5
  kind: "generator",
6
6
  description: "CRUD server generator with routes, actions, and persistence scaffolding.",
7
7
  options: {
@@ -152,14 +152,14 @@ export default Object.freeze({
152
152
  mutations: {
153
153
  dependencies: {
154
154
  runtime: {
155
- "@jskit-ai/auth-core": "0.1.56",
156
- "@jskit-ai/crud-core": "0.1.65",
157
- "@jskit-ai/database-runtime": "0.1.57",
158
- "@jskit-ai/http-runtime": "0.1.56",
159
- "@jskit-ai/json-rest-api-core": "0.1.2",
160
- "@jskit-ai/kernel": "0.1.57",
161
- "@jskit-ai/realtime": "0.1.56",
162
- "@jskit-ai/resource-crud-core": "0.1.2",
155
+ "@jskit-ai/auth-core": "0.1.58",
156
+ "@jskit-ai/crud-core": "0.1.67",
157
+ "@jskit-ai/database-runtime": "0.1.59",
158
+ "@jskit-ai/http-runtime": "0.1.58",
159
+ "@jskit-ai/json-rest-api-core": "0.1.4",
160
+ "@jskit-ai/kernel": "0.1.59",
161
+ "@jskit-ai/realtime": "0.1.58",
162
+ "@jskit-ai/resource-crud-core": "0.1.4",
163
163
  "@local/${option:namespace|kebab}": "file:packages/${option:namespace|kebab}"
164
164
  },
165
165
  dev: {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/crud-server-generator",
3
- "version": "0.1.65",
3
+ "version": "0.1.67",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -13,12 +13,12 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@babel/parser": "^7.29.2",
16
- "@jskit-ai/crud-core": "0.1.65",
17
- "@jskit-ai/database-runtime": "0.1.57",
18
- "@jskit-ai/http-runtime": "0.1.56",
19
- "@jskit-ai/json-rest-api-core": "0.1.2",
20
- "@jskit-ai/kernel": "0.1.57",
21
- "@jskit-ai/resource-crud-core": "0.1.2",
16
+ "@jskit-ai/crud-core": "0.1.67",
17
+ "@jskit-ai/database-runtime": "0.1.59",
18
+ "@jskit-ai/http-runtime": "0.1.58",
19
+ "@jskit-ai/json-rest-api-core": "0.1.4",
20
+ "@jskit-ai/kernel": "0.1.59",
21
+ "@jskit-ai/resource-crud-core": "0.1.4",
22
22
  "recast": "^0.23.11"
23
23
  }
24
24
  }
@@ -31,6 +31,16 @@ const BABEL_REC_AST_PARSER = Object.freeze({
31
31
  });
32
32
 
33
33
  const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
34
+ const EXPLICIT_CRUD_SCHEMA_OVERRIDE_KEYS = Object.freeze([
35
+ "body",
36
+ "output",
37
+ "listOutput",
38
+ "listItemOutput",
39
+ "createBody",
40
+ "replaceBody",
41
+ "patchBody",
42
+ "deleteOutput"
43
+ ]);
34
44
 
35
45
  function isIdentifierName(value = "") {
36
46
  return IDENTIFIER_PATTERN.test(String(value || ""));
@@ -138,6 +148,7 @@ function requireCrudResourceConfigObject(programNode, context = "crud-server-gen
138
148
 
139
149
  function requireResourceSchemaObject(programNode, context = "crud-server-generator scaffold-field") {
140
150
  const resourceObject = requireCrudResourceConfigObject(programNode, context);
151
+ assertNoExplicitCrudSchemaOverrides(resourceObject, context);
141
152
  const schemaProperty = findObjectPropertyByName(resourceObject, "schema");
142
153
  if (!schemaProperty || !n.ObjectExpression.check(schemaProperty.value)) {
143
154
  throw new Error(
@@ -148,6 +159,30 @@ function requireResourceSchemaObject(programNode, context = "crud-server-generat
148
159
  return schemaProperty.value;
149
160
  }
150
161
 
162
+ function assertNoExplicitCrudSchemaOverrides(resourceObject, context = "crud-server-generator scaffold-field") {
163
+ const crudProperty = findObjectPropertyByName(resourceObject, "crud");
164
+ if (!crudProperty) {
165
+ return;
166
+ }
167
+
168
+ if (!n.ObjectExpression.check(crudProperty.value)) {
169
+ throw new Error(
170
+ `${context} cannot patch defineCrudResource({ ..., crud: ... }) unless crud is omitted or authored as an inline object literal without explicit schema overrides.`
171
+ );
172
+ }
173
+
174
+ const overrideKeys = EXPLICIT_CRUD_SCHEMA_OVERRIDE_KEYS.filter((propertyName) =>
175
+ hasObjectProperty(crudProperty.value, propertyName)
176
+ );
177
+ if (overrideKeys.length === 0) {
178
+ return;
179
+ }
180
+
181
+ throw new Error(
182
+ `${context} cannot patch defineCrudResource({ ..., crud: { ... } }) when explicit crud schema overrides are authored (${overrideKeys.join(", ")}). Update schema and override validators manually.`
183
+ );
184
+ }
185
+
151
186
  function findObjectPropertyByName(objectNode, propertyName = "") {
152
187
  const targetName = normalizeText(propertyName);
153
188
  if (!targetName || !n.ObjectExpression.check(objectNode)) {
@@ -6,7 +6,7 @@ import {
6
6
  returnNullWhenJsonRestResourceMissing
7
7
  } from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
8
8
  import { resource } from "../shared/${option:namespace|singular|camel}Resource.js";
9
- const RESOURCE_TYPE = resource.namespace;
9
+ const JSON_REST_SCOPE_NAME = __JSKIT_CRUD_JSONREST_SCOPE_NAME__;
10
10
 
11
11
  function createRepository({ api, knex } = {}) {
12
12
  const withTransaction = createWithTransaction(knex);
@@ -14,7 +14,7 @@ function createRepository({ api, knex } = {}) {
14
14
  async function queryDocuments(query = {}, options = {}) {
15
15
  return api.resources.${option:namespace|camel}.query(
16
16
  {
17
- queryParams: buildJsonRestQueryParams(RESOURCE_TYPE, query),
17
+ queryParams: buildJsonRestQueryParams(JSON_REST_SCOPE_NAME, query),
18
18
  transaction: options?.trx || null,
19
19
  simplified: false
20
20
  },
@@ -27,7 +27,7 @@ function createRepository({ api, knex } = {}) {
27
27
  api.resources.${option:namespace|camel}.get(
28
28
  {
29
29
  id: recordId,
30
- queryParams: buildJsonRestQueryParams(RESOURCE_TYPE, {}, {
30
+ queryParams: buildJsonRestQueryParams(JSON_REST_SCOPE_NAME, {}, {
31
31
  include: options?.include
32
32
  }),
33
33
  transaction: options?.trx || null,
@@ -41,7 +41,9 @@ function createRepository({ api, knex } = {}) {
41
41
  async function createDocument(payload = {}, options = {}) {
42
42
  return api.resources.${option:namespace|camel}.post(
43
43
  {
44
- inputRecord: createJsonApiInputRecord(RESOURCE_TYPE, payload),
44
+ inputRecord: createJsonApiInputRecord(JSON_REST_SCOPE_NAME, payload, {
45
+ resource
46
+ }),
45
47
  transaction: options?.trx || null,
46
48
  simplified: false
47
49
  },
@@ -60,10 +62,13 @@ function createRepository({ api, knex } = {}) {
60
62
  {
61
63
  id: recordId,
62
64
  inputRecord: createJsonApiInputRecord(
63
- RESOURCE_TYPE,
65
+ JSON_REST_SCOPE_NAME,
64
66
  {
65
67
  ...sourcePatch,
66
68
  updatedAt: new Date()
69
+ },
70
+ {
71
+ resource
67
72
  }
68
73
  ),
69
74
  transaction: options?.trx || null,
@@ -85,7 +90,7 @@ function createRepository({ api, knex } = {}) {
85
90
  createJsonRestContext(options?.context || null)
86
91
  );
87
92
 
88
- return true;
93
+ return null;
89
94
  });
90
95
  }
91
96
 
@@ -43,11 +43,10 @@ function createService({ ${option:namespace|camel}Repository } = {}) {
43
43
  }
44
44
 
45
45
  async function deleteDocumentById(recordId, options = {}) {
46
- return404IfNotFound(await ${option:namespace|camel}Repository.deleteDocumentById(recordId, {
46
+ return ${option:namespace|camel}Repository.deleteDocumentById(recordId, {
47
47
  trx: options?.trx || null,
48
48
  context: options?.context || null
49
- }));
50
- return null;
49
+ });
51
50
  }
52
51
 
53
52
  return Object.freeze({
@@ -78,6 +78,38 @@ const resource = defineCrudResource({
78
78
  export { resource };
79
79
  `;
80
80
 
81
+ const EXPLICIT_CRUD_OVERRIDE_RESOURCE_SOURCE = `import { createSchema } from "json-rest-schema";
82
+ import { defineCrudResource } from "@jskit-ai/resource-crud-core/shared/crudResource";
83
+
84
+ const recordOutputSchema = createSchema({
85
+ id: {
86
+ type: "string",
87
+ required: true
88
+ }
89
+ });
90
+
91
+ const resource = defineCrudResource({
92
+ namespace: "contacts",
93
+ tableName: "contacts",
94
+ schema: {
95
+ firstName: {
96
+ type: "string",
97
+ required: true,
98
+ operations: {
99
+ output: { required: true },
100
+ create: { required: true },
101
+ patch: { required: false }
102
+ }
103
+ }
104
+ },
105
+ crud: {
106
+ output: recordOutputSchema
107
+ }
108
+ });
109
+
110
+ export { resource };
111
+ `;
112
+
81
113
  async function withTempApp(run) {
82
114
  const appRoot = await mkdtemp(path.join(tmpdir(), "crud-server-scaffold-field-"));
83
115
  try {
@@ -194,3 +226,21 @@ test("scaffold-field rejects resource modules without an inline schema object li
194
226
  );
195
227
  });
196
228
  });
229
+
230
+ test("scaffold-field rejects defineCrudResource modules with explicit crud schema overrides", async () => {
231
+ await withTempApp(async (appRoot) => {
232
+ const resourceFile = "packages/contacts/src/shared/contactResource.js";
233
+ await writeAppFile(appRoot, resourceFile, EXPLICIT_CRUD_OVERRIDE_RESOURCE_SOURCE);
234
+
235
+ await assert.rejects(
236
+ () => runGeneratorSubcommand({
237
+ appRoot,
238
+ subcommand: "scaffold-field",
239
+ args: ["categoryId", resourceFile],
240
+ options: {},
241
+ resolveSnapshot: async () => createSnapshot()
242
+ }),
243
+ /cannot patch defineCrudResource\(\{ \.\.\., crud: \{ \.\.\. \} \}\) when explicit crud schema overrides are authored \(output\)\. Update schema and override validators manually\./
244
+ );
245
+ });
246
+ });
@@ -958,6 +958,7 @@ test("crud repository template defines a json-rest-api adapter over the injected
958
958
  const templatePath = path.resolve(testDirectory, "..", "templates", "src", "local-package", "server", "repository.js");
959
959
  const templateSource = await readFile(templatePath, "utf8");
960
960
  assert.doesNotMatch(templateSource, /from "@jskit-ai\/http-runtime\/shared";/);
961
+ assert.match(templateSource, /const JSON_REST_SCOPE_NAME = __JSKIT_CRUD_JSONREST_SCOPE_NAME__;/);
961
962
  assert.match(templateSource, /returnNullWhenJsonRestResourceMissing/);
962
963
  assert.match(templateSource, /return api\.resources\.\$\{option:namespace\|camel\}\.query\(/);
963
964
  assert.match(templateSource, /return returnNullWhenJsonRestResourceMissing\(\(\) =>\s+api\.resources\.\$\{option:namespace\|camel\}\.get\(/s);
@@ -965,8 +966,8 @@ test("crud repository template defines a json-rest-api adapter over the injected
965
966
  assert.match(templateSource, /return returnNullWhenJsonRestResourceMissing\(\(\) =>\s+api\.resources\.\$\{option:namespace\|camel\}\.patch\(/s);
966
967
  assert.match(templateSource, /return returnNullWhenJsonRestResourceMissing\(async \(\) => \{\s+await api\.resources\.\$\{option:namespace\|camel\}\.delete\(/s);
967
968
  assert.match(templateSource, /createJsonRestContext\(options\?\.context \|\| null\)/);
968
- assert.match(templateSource, /buildJsonRestQueryParams\(RESOURCE_TYPE, query\)/);
969
- assert.match(templateSource, /createJsonApiInputRecord\(RESOURCE_TYPE, payload\)/);
969
+ assert.match(templateSource, /buildJsonRestQueryParams\(JSON_REST_SCOPE_NAME, query\)/);
970
+ assert.match(templateSource, /createJsonApiInputRecord\(JSON_REST_SCOPE_NAME, payload/);
970
971
  assert.doesNotMatch(templateSource, /function toJsonRestContext\(context = null\)/);
971
972
  assert.doesNotMatch(templateSource, /function normalizeArrayInput\(value\)/);
972
973
  assert.doesNotMatch(templateSource, /function buildJsonRestQueryParams\(query = \{\}/);
@@ -1039,6 +1040,9 @@ test("crud service template preserves JSON:API output and emits entity ids from
1039
1040
  assert.match(templateSource, /returnJsonApiDocument\(await \$\{option:namespace\|camel\}Repository\.queryDocuments\(query, \{/);
1040
1041
  assert.match(templateSource, /async function patchDocumentById\(recordId, payload = \{\}, options = \{\}\)/);
1041
1042
  assert.match(templateSource, /returnJsonApiDocument\(return404IfNotFound\(await \$\{option:namespace\|camel\}Repository\.patchDocumentById\(recordId, payload, \{/);
1043
+ assert.match(templateSource, /async function deleteDocumentById\(recordId, options = \{\}\)/);
1044
+ assert.match(templateSource, /return \$\{option:namespace\|camel\}Repository\.deleteDocumentById\(recordId, \{/);
1045
+ assert.doesNotMatch(templateSource, /return404IfNotFound\(await \$\{option:namespace\|camel\}Repository\.deleteDocumentById/);
1042
1046
  assert.match(templateSource, /return Object\.freeze\(\{/);
1043
1047
  assert.match(templateSource, /export \{ createService \};/);
1044
1048
  });
@@ -117,6 +117,50 @@ test("template createRepository builds mutable JSON:API input documents for writ
117
117
  });
118
118
  });
119
119
 
120
+ test("template createRepository returns null for successful deletes", async () => {
121
+ const calls = [];
122
+ const api = {
123
+ resources: {
124
+ customers: {
125
+ async delete(params, context) {
126
+ calls.push({ params, context });
127
+ }
128
+ }
129
+ }
130
+ };
131
+ const knex = {
132
+ async transaction(work) {
133
+ return work("trx");
134
+ }
135
+ };
136
+
137
+ const repository = createRepository({ api, knex });
138
+ const result = await repository.deleteDocumentById("7", {
139
+ trx: "trx-1",
140
+ context: {
141
+ visibilityContext: {
142
+ visibility: "workspace",
143
+ scopeOwnerId: "7"
144
+ }
145
+ }
146
+ });
147
+
148
+ assert.equal(result, null);
149
+ assert.deepEqual(calls[0], {
150
+ params: {
151
+ id: "7",
152
+ transaction: "trx-1",
153
+ simplified: false
154
+ },
155
+ context: {
156
+ visibilityContext: {
157
+ visibility: "workspace",
158
+ scopeOwnerId: "7"
159
+ }
160
+ }
161
+ });
162
+ });
163
+
120
164
  test("template createService turns missing resource records into 404 errors", async () => {
121
165
  const service = createService({
122
166
  customersRepository: {
@@ -139,6 +183,45 @@ test("template createService turns missing resource records into 404 errors", as
139
183
  );
140
184
  });
141
185
 
186
+ test("template createService returns delete results unchanged", async () => {
187
+ const service = createService({
188
+ customersRepository: {
189
+ async deleteDocumentById(recordId, options) {
190
+ return {
191
+ recordId,
192
+ options,
193
+ deleted: true
194
+ };
195
+ }
196
+ }
197
+ });
198
+
199
+ assert.deepEqual(
200
+ await service.deleteDocumentById("7", {
201
+ trx: "trx-1",
202
+ context: {
203
+ visibilityContext: {
204
+ visibility: "workspace",
205
+ scopeOwnerId: "7"
206
+ }
207
+ }
208
+ }),
209
+ {
210
+ recordId: "7",
211
+ options: {
212
+ trx: "trx-1",
213
+ context: {
214
+ visibilityContext: {
215
+ visibility: "workspace",
216
+ scopeOwnerId: "7"
217
+ }
218
+ }
219
+ },
220
+ deleted: true
221
+ }
222
+ );
223
+ });
224
+
142
225
  test("template createActions requires namespaced CRUD permissions by default", () => {
143
226
  const actions = createActions({ surface: "admin" });
144
227
 
@@ -30,7 +30,7 @@ test("crudService exposes the explicit JSON:API CRUD service contract", async ()
30
30
  },
31
31
  async deleteDocumentById(recordId, options) {
32
32
  calls.push(["deleteDocumentById", recordId, options]);
33
- return true;
33
+ return null;
34
34
  }
35
35
  };
36
36
 
@@ -131,9 +131,42 @@ test("crudService throws 404 when a document is missing", async () => {
131
131
  () => service.patchDocumentById(9, { textField: "Changed" }, {}),
132
132
  (error) => error?.status === 404 && error?.message === "Document not found."
133
133
  );
134
+ });
134
135
 
135
- await assert.rejects(
136
- () => service.deleteDocumentById(9, {}),
137
- (error) => error?.status === 404 && error?.message === "Document not found."
138
- );
136
+ test("crudService returns delete results unchanged", async () => {
137
+ const service = createService({
138
+ customersRepository: {
139
+ async deleteDocumentById(recordId, options) {
140
+ return {
141
+ recordId,
142
+ options,
143
+ deleted: true
144
+ };
145
+ }
146
+ }
147
+ });
148
+
149
+ const result = await service.deleteDocumentById(9, {
150
+ trx: "trx-1",
151
+ context: {
152
+ visibilityContext: {
153
+ visibility: "workspace",
154
+ scopeOwnerId: "7"
155
+ }
156
+ }
157
+ });
158
+
159
+ assert.deepEqual(result, {
160
+ recordId: 9,
161
+ options: {
162
+ trx: "trx-1",
163
+ context: {
164
+ visibilityContext: {
165
+ visibility: "workspace",
166
+ scopeOwnerId: "7"
167
+ }
168
+ }
169
+ },
170
+ deleted: true
171
+ });
139
172
  });