@jskit-ai/crud-core 0.1.63 → 0.1.65

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,146 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createSchema } from "json-rest-schema";
4
+ import {
5
+ validateSchemaPayload
6
+ } from "@jskit-ai/kernel/shared/validators";
7
+ import { createCrudJsonApiRouteContracts } from "../src/server/routeContracts.js";
8
+
9
+ function createSchemaDefinition(structure = {}, mode = "patch") {
10
+ return Object.freeze({
11
+ schema: createSchema(structure),
12
+ mode
13
+ });
14
+ }
15
+
16
+ function createCrudResource() {
17
+ return Object.freeze({
18
+ namespace: "contacts",
19
+ defaultSort: Object.freeze(["-createdAt"]),
20
+ operations: Object.freeze({
21
+ view: Object.freeze({
22
+ output: createSchemaDefinition({
23
+ id: {
24
+ type: "string",
25
+ required: true
26
+ },
27
+ contactId: {
28
+ type: "string",
29
+ required: false,
30
+ relation: {
31
+ kind: "lookup",
32
+ apiPath: "/contacts",
33
+ valueKey: "id"
34
+ }
35
+ },
36
+ name: {
37
+ type: "string",
38
+ required: true
39
+ }
40
+ }, "replace")
41
+ }),
42
+ create: Object.freeze({
43
+ body: createSchemaDefinition({
44
+ contactId: {
45
+ type: "string",
46
+ required: false,
47
+ relation: {
48
+ kind: "lookup",
49
+ apiPath: "/contacts",
50
+ valueKey: "id"
51
+ }
52
+ },
53
+ name: {
54
+ type: "string",
55
+ required: true
56
+ }
57
+ }, "create"),
58
+ output: createSchemaDefinition({
59
+ id: {
60
+ type: "string",
61
+ required: true
62
+ },
63
+ name: {
64
+ type: "string",
65
+ required: true
66
+ }
67
+ }, "replace")
68
+ }),
69
+ patch: Object.freeze({
70
+ body: createSchemaDefinition({
71
+ name: {
72
+ type: "string",
73
+ required: false
74
+ }
75
+ }, "patch"),
76
+ output: createSchemaDefinition({
77
+ id: {
78
+ type: "string",
79
+ required: true
80
+ },
81
+ name: {
82
+ type: "string",
83
+ required: true
84
+ }
85
+ }, "replace")
86
+ })
87
+ })
88
+ });
89
+ }
90
+
91
+ test("createCrudJsonApiRouteContracts builds default CRUD JSON:API contracts", async () => {
92
+ const resource = createCrudResource();
93
+ const routeParamsValidator = createSchemaDefinition({
94
+ workspaceSlug: {
95
+ type: "string",
96
+ required: true
97
+ }
98
+ });
99
+
100
+ const contracts = createCrudJsonApiRouteContracts({
101
+ resource,
102
+ routeParamsValidator
103
+ });
104
+
105
+ assert.equal(contracts.listRouteContract.transport.contentType, "application/vnd.api+json");
106
+ assert.equal(contracts.viewRouteContract.transport.contentType, "application/vnd.api+json");
107
+ assert.equal(contracts.createRouteContract.transport.contentType, "application/vnd.api+json");
108
+ assert.equal(contracts.updateRouteContract.transport.contentType, "application/vnd.api+json");
109
+ assert.equal(contracts.listRouteContract.responses[200].transportSchema.type, "object");
110
+ assert.equal(contracts.createRouteContract.responses[201].transportSchema.type, "object");
111
+ assert.equal(contracts.updateRouteContract.responses[200].transportSchema.type, "object");
112
+ assert.equal(Object.hasOwn(contracts.deleteRouteContract.responses, "204"), false);
113
+ assert.equal(contracts.createRouteContract.body, resource.operations.create.body);
114
+ assert.equal(contracts.updateRouteContract.body, resource.operations.patch.body);
115
+ assert.deepEqual(
116
+ Object.keys(contracts.recordRouteParamsValidator.schema.getFieldDefinitions()).sort(),
117
+ ["recordId", "workspaceSlug"]
118
+ );
119
+
120
+ const normalizedListQuery = await validateSchemaPayload(contracts.listRouteContract.query, {
121
+ q: " hello ",
122
+ include: " ownerId ",
123
+ contactId: " 42 ",
124
+ cursor: " offset:2 ",
125
+ limit: "25"
126
+ }, { phase: "input" });
127
+
128
+ assert.deepEqual(normalizedListQuery, {
129
+ q: "hello",
130
+ include: "ownerId",
131
+ contactId: "42",
132
+ cursor: "offset:2",
133
+ limit: 25
134
+ });
135
+ });
136
+
137
+ test("createCrudJsonApiRouteContracts falls back to recordId params when no route params validator is provided", () => {
138
+ const contracts = createCrudJsonApiRouteContracts({
139
+ resource: createCrudResource()
140
+ });
141
+
142
+ assert.deepEqual(
143
+ Object.keys(contracts.recordRouteParamsValidator.schema.getFieldDefinitions()),
144
+ ["recordId"]
145
+ );
146
+ });
@@ -1,6 +1,12 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { createCrudServiceEvents } from "../src/server/serviceEvents.js";
3
+ import {
4
+ createCrudJsonApiServiceEvents,
5
+ createCrudServiceEvents,
6
+ resolveCrudEntityIdFromArgs,
7
+ resolveCrudEntityIdFromResult,
8
+ resolveCrudJsonApiEntityIdFromResult
9
+ } from "../src/server/serviceEvents.js";
4
10
 
5
11
  test("createCrudServiceEvents builds CRUD realtime events from resource namespace", () => {
6
12
  const events = createCrudServiceEvents({
@@ -26,3 +32,37 @@ test("createCrudServiceEvents validates required resource namespace", () => {
26
32
  /resource\.namespace/
27
33
  );
28
34
  });
35
+
36
+ test("createCrudJsonApiServiceEvents builds JSON:API-aware create/update/delete event defaults", () => {
37
+ const events = createCrudJsonApiServiceEvents("contacts");
38
+
39
+ assert.equal(events.createDocument[0].realtime.event, "contacts.record.changed");
40
+ assert.equal(events.patchDocumentById[0].realtime.event, "contacts.record.changed");
41
+ assert.equal(events.deleteDocumentById[0].realtime.event, "contacts.record.changed");
42
+ assert.equal(events.createDocument[0].entityId({
43
+ result: {
44
+ data: {
45
+ id: "21"
46
+ }
47
+ }
48
+ }), "21");
49
+ assert.equal(events.patchDocumentById[0].entityId({
50
+ args: [22]
51
+ }), "22");
52
+ assert.equal(events.deleteDocumentById[0].entityId({
53
+ args: [23]
54
+ }), "23");
55
+ });
56
+
57
+ test("service event entity-id helpers normalize ids from args, plain results, and JSON:API results", () => {
58
+ assert.equal(resolveCrudEntityIdFromArgs({ args: [12] }), "12");
59
+ assert.equal(resolveCrudEntityIdFromResult({ result: { id: 13 } }), "13");
60
+ assert.equal(resolveCrudJsonApiEntityIdFromResult({
61
+ result: {
62
+ data: {
63
+ id: 14
64
+ }
65
+ }
66
+ }), "14");
67
+ assert.equal(resolveCrudEntityIdFromArgs({ args: [" "] }), "");
68
+ });
@@ -1,5 +1,6 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
+ import { createSchema } from "json-rest-schema";
3
4
  import {
4
5
  createCrudServiceRuntime,
5
6
  crudServiceListRecords,
@@ -9,21 +10,22 @@ import {
9
10
  crudServiceDeleteRecord
10
11
  } from "../src/server/serviceMethods.js";
11
12
 
13
+ function createOperationSchemaDefinition(structure = {}, mode = "replace") {
14
+ return {
15
+ schema: createSchema(structure),
16
+ mode
17
+ };
18
+ }
19
+
12
20
  function createResourceWithOutputSchema(overrides = {}) {
13
21
  return {
14
22
  namespace: "contacts",
15
23
  operations: {
16
24
  view: {
17
- outputValidator: {
18
- schema: {
19
- type: "object",
20
- properties: {
21
- id: { type: "integer" },
22
- name: { type: "string" }
23
- },
24
- required: ["id", "name"]
25
- }
26
- }
25
+ output: createOperationSchemaDefinition({
26
+ id: { type: "integer", required: true },
27
+ name: { type: "string", required: true }
28
+ })
27
29
  }
28
30
  },
29
31
  ...overrides
@@ -1,153 +0,0 @@
1
- import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
- import {
3
- DEFAULT_CRUD_LOOKUP_CONTAINER_KEY,
4
- normalizeCrudLookupContainerKey
5
- } from "@jskit-ai/kernel/shared/support/crudLookup";
6
-
7
- const CRUD_RUNTIME_LOOKUPS_FIELD_KEY = DEFAULT_CRUD_LOOKUP_CONTAINER_KEY;
8
- const CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE = "autocomplete";
9
- const CRUD_LOOKUP_FORM_CONTROL_SELECT = "select";
10
- const CRUD_FIELD_REPOSITORY_STORAGE_COLUMN = "column";
11
- const CRUD_FIELD_REPOSITORY_STORAGE_VIRTUAL = "virtual";
12
- const CRUD_FIELD_REPOSITORY_WRITE_SERIALIZER_DATETIME_UTC = "datetime-utc";
13
-
14
- function normalizeCrudFieldRepositoryWriteSerializer(
15
- value,
16
- {
17
- context = "crud fieldMeta repository",
18
- fieldKey = ""
19
- } = {}
20
- ) {
21
- const normalizedFieldKey = normalizeText(fieldKey);
22
- const normalizedValue = normalizeText(value).toLowerCase();
23
- if (!normalizedValue) {
24
- return "";
25
- }
26
-
27
- if (normalizedValue === CRUD_FIELD_REPOSITORY_WRITE_SERIALIZER_DATETIME_UTC) {
28
- return normalizedValue;
29
- }
30
-
31
- throw new Error(
32
- `${context}${normalizedFieldKey ? `["${normalizedFieldKey}"]` : ""} repository.writeSerializer must be ` +
33
- `"${CRUD_FIELD_REPOSITORY_WRITE_SERIALIZER_DATETIME_UTC}" when provided.`
34
- );
35
- }
36
-
37
- function checkCrudLookupFormControl(
38
- value,
39
- {
40
- context = "crud fieldMeta ui.formControl",
41
- defaultValue = CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE
42
- } = {}
43
- ) {
44
- const resolvedValue = value === undefined || value === null || value === "" ? defaultValue : value;
45
- if (resolvedValue === "") {
46
- return "";
47
- }
48
-
49
- if (
50
- resolvedValue === CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE ||
51
- resolvedValue === CRUD_LOOKUP_FORM_CONTROL_SELECT
52
- ) {
53
- return resolvedValue;
54
- }
55
-
56
- throw new Error(
57
- `${context} must be "${CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE}" or "${CRUD_LOOKUP_FORM_CONTROL_SELECT}". ` +
58
- `Received: ${JSON.stringify(resolvedValue)}.`
59
- );
60
- }
61
-
62
- function isCrudRuntimeOutputOnlyFieldKey(
63
- value = "",
64
- {
65
- lookupContainerKey = CRUD_RUNTIME_LOOKUPS_FIELD_KEY
66
- } = {}
67
- ) {
68
- const resolvedLookupContainerKey = normalizeCrudLookupContainerKey(lookupContainerKey, {
69
- context: "crud runtime lookup container key"
70
- });
71
- return normalizeText(value) === resolvedLookupContainerKey;
72
- }
73
-
74
- function normalizeCrudFieldRepositoryConfig(
75
- fieldMetaEntry = {},
76
- {
77
- context = "crud fieldMeta repository",
78
- fieldKey = ""
79
- } = {}
80
- ) {
81
- const normalizedFieldKey = normalizeText(fieldKey || fieldMetaEntry?.key);
82
- const repository = fieldMetaEntry?.repository;
83
- if (repository === undefined || repository === null) {
84
- return Object.freeze({
85
- storage: CRUD_FIELD_REPOSITORY_STORAGE_COLUMN,
86
- column: ""
87
- });
88
- }
89
- if (!repository || typeof repository !== "object" || Array.isArray(repository)) {
90
- throw new TypeError(
91
- `${context}${normalizedFieldKey ? `["${normalizedFieldKey}"]` : ""} must be an object when provided.`
92
- );
93
- }
94
-
95
- const repositoryKeys = Object.keys(repository);
96
- for (const repositoryKey of repositoryKeys) {
97
- if (repositoryKey !== "column" && repositoryKey !== "storage" && repositoryKey !== "writeSerializer") {
98
- throw new Error(
99
- `${context}${normalizedFieldKey ? `["${normalizedFieldKey}"]` : ""} does not support repository.${repositoryKey}.`
100
- );
101
- }
102
- }
103
-
104
- const column = normalizeText(repository.column);
105
- const storage = normalizeText(repository.storage).toLowerCase();
106
- const writeSerializer = normalizeCrudFieldRepositoryWriteSerializer(repository.writeSerializer, {
107
- context,
108
- fieldKey: normalizedFieldKey
109
- });
110
-
111
- if (!column && !storage && !writeSerializer) {
112
- throw new Error(
113
- `${context}${normalizedFieldKey ? `["${normalizedFieldKey}"]` : ""} requires repository.column, repository.storage, or repository.writeSerializer.`
114
- );
115
- }
116
-
117
- if (storage && storage !== CRUD_FIELD_REPOSITORY_STORAGE_VIRTUAL) {
118
- throw new Error(
119
- `${context}${normalizedFieldKey ? `["${normalizedFieldKey}"]` : ""} repository.storage must be "virtual" when provided.`
120
- );
121
- }
122
- if (storage === CRUD_FIELD_REPOSITORY_STORAGE_VIRTUAL && column) {
123
- throw new Error(
124
- `${context}${normalizedFieldKey ? `["${normalizedFieldKey}"]` : ""} repository.storage "virtual" cannot define repository.column.`
125
- );
126
- }
127
- if (storage === CRUD_FIELD_REPOSITORY_STORAGE_VIRTUAL && writeSerializer) {
128
- throw new Error(
129
- `${context}${normalizedFieldKey ? `["${normalizedFieldKey}"]` : ""} repository.storage "virtual" cannot define repository.writeSerializer.`
130
- );
131
- }
132
-
133
- return Object.freeze({
134
- storage: storage === CRUD_FIELD_REPOSITORY_STORAGE_VIRTUAL
135
- ? CRUD_FIELD_REPOSITORY_STORAGE_VIRTUAL
136
- : CRUD_FIELD_REPOSITORY_STORAGE_COLUMN,
137
- column,
138
- writeSerializer
139
- });
140
- }
141
-
142
- export {
143
- CRUD_FIELD_REPOSITORY_STORAGE_COLUMN,
144
- CRUD_FIELD_REPOSITORY_STORAGE_VIRTUAL,
145
- CRUD_FIELD_REPOSITORY_WRITE_SERIALIZER_DATETIME_UTC,
146
- CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE,
147
- CRUD_LOOKUP_FORM_CONTROL_SELECT,
148
- CRUD_RUNTIME_LOOKUPS_FIELD_KEY,
149
- checkCrudLookupFormControl,
150
- isCrudRuntimeOutputOnlyFieldKey,
151
- normalizeCrudFieldRepositoryConfig,
152
- normalizeCrudFieldRepositoryWriteSerializer
153
- };