@jskit-ai/resource-core 0.1.1
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.
- package/package.descriptor.mjs +36 -0
- package/package.json +14 -0
- package/src/shared/resource.js +197 -0
- package/test/resource.test.js +105 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export default Object.freeze({
|
|
2
|
+
packageVersion: 1,
|
|
3
|
+
packageId: "@jskit-ai/resource-core",
|
|
4
|
+
version: "0.1.1",
|
|
5
|
+
kind: "runtime",
|
|
6
|
+
description: "Generic resource-definition helpers and schema-definition normalization.",
|
|
7
|
+
dependsOn: [
|
|
8
|
+
"@jskit-ai/kernel"
|
|
9
|
+
],
|
|
10
|
+
capabilities: {
|
|
11
|
+
provides: ["resource.core"],
|
|
12
|
+
requires: []
|
|
13
|
+
},
|
|
14
|
+
runtime: {
|
|
15
|
+
server: {
|
|
16
|
+
providers: []
|
|
17
|
+
},
|
|
18
|
+
client: {
|
|
19
|
+
providers: []
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
mutations: {
|
|
23
|
+
dependencies: {
|
|
24
|
+
runtime: {
|
|
25
|
+
"@jskit-ai/resource-core": "0.1.1"
|
|
26
|
+
},
|
|
27
|
+
dev: {}
|
|
28
|
+
},
|
|
29
|
+
packageJson: {
|
|
30
|
+
scripts: {}
|
|
31
|
+
},
|
|
32
|
+
procfile: {},
|
|
33
|
+
files: [],
|
|
34
|
+
text: []
|
|
35
|
+
}
|
|
36
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jskit-ai/resource-core",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "node --test"
|
|
7
|
+
},
|
|
8
|
+
"exports": {
|
|
9
|
+
"./shared/resource": "./src/shared/resource.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@jskit-ai/kernel": "0.1.56"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
|
|
2
|
+
import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
|
+
|
|
4
|
+
const SCHEMA_DEFINITION_MODES = new Set(["create", "replace", "patch"]);
|
|
5
|
+
const OPERATION_SCHEMA_SECTION_NAMES = Object.freeze([
|
|
6
|
+
"body",
|
|
7
|
+
"query",
|
|
8
|
+
"params",
|
|
9
|
+
"input",
|
|
10
|
+
"output"
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
function isJsonRestSchemaInstance(value) {
|
|
14
|
+
return Boolean(value) &&
|
|
15
|
+
typeof value === "object" &&
|
|
16
|
+
typeof value.create === "function" &&
|
|
17
|
+
typeof value.replace === "function" &&
|
|
18
|
+
typeof value.patch === "function" &&
|
|
19
|
+
typeof value.toJsonSchema === "function";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isSchemaDefinitionLike(value) {
|
|
23
|
+
return Boolean(value) &&
|
|
24
|
+
typeof value === "object" &&
|
|
25
|
+
!Array.isArray(value) &&
|
|
26
|
+
(
|
|
27
|
+
Object.hasOwn(value, "schema") ||
|
|
28
|
+
Object.hasOwn(value, "mode")
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveSchemaDefinitionMode(mode, {
|
|
33
|
+
context = "schema definition.mode",
|
|
34
|
+
defaultMode = "patch"
|
|
35
|
+
} = {}) {
|
|
36
|
+
const fallbackMode = normalizeText(defaultMode).toLowerCase() || "patch";
|
|
37
|
+
const normalizedMode = normalizeText(mode).toLowerCase() || fallbackMode;
|
|
38
|
+
|
|
39
|
+
if (!SCHEMA_DEFINITION_MODES.has(normalizedMode)) {
|
|
40
|
+
throw new TypeError(`${context} must be one of: create, replace, patch.`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return normalizedMode;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createSchemaDefinition(schema, mode = "patch", {
|
|
47
|
+
context = "schema definition"
|
|
48
|
+
} = {}) {
|
|
49
|
+
if (!isJsonRestSchemaInstance(schema)) {
|
|
50
|
+
throw new TypeError(`${context}.schema must be a json-rest-schema schema instance.`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return Object.freeze({
|
|
54
|
+
schema,
|
|
55
|
+
mode: resolveSchemaDefinitionMode(mode, {
|
|
56
|
+
context: `${context}.mode`,
|
|
57
|
+
defaultMode: "patch"
|
|
58
|
+
})
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeSchemaDefinitionLike(value, {
|
|
63
|
+
context = "schema definition",
|
|
64
|
+
defaultMode = "patch"
|
|
65
|
+
} = {}) {
|
|
66
|
+
if (value == null) {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isJsonRestSchemaInstance(value)) {
|
|
71
|
+
return createSchemaDefinition(value, defaultMode, { context });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!isSchemaDefinitionLike(value)) {
|
|
75
|
+
throw new TypeError(
|
|
76
|
+
`${context} must be a json-rest-schema schema instance or schema definition object.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const source = normalizeObject(value);
|
|
81
|
+
|
|
82
|
+
if (!Object.hasOwn(source, "schema")) {
|
|
83
|
+
throw new TypeError(`${context}.schema is required.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return createSchemaDefinition(
|
|
87
|
+
source.schema,
|
|
88
|
+
Object.hasOwn(source, "mode") ? source.mode : defaultMode,
|
|
89
|
+
{ context }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolveDefaultOperationSchemaMode(sectionName, operation = {}) {
|
|
94
|
+
const normalizedSectionName = normalizeText(sectionName).toLowerCase();
|
|
95
|
+
if (normalizedSectionName === "output") {
|
|
96
|
+
return "replace";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (normalizedSectionName !== "body") {
|
|
100
|
+
return "patch";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const normalizedMethod = normalizeText(operation?.method).toUpperCase();
|
|
104
|
+
if (normalizedMethod === "POST") {
|
|
105
|
+
return "create";
|
|
106
|
+
}
|
|
107
|
+
if (normalizedMethod === "PUT") {
|
|
108
|
+
return "replace";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return "patch";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function normalizeOperationDefinition(operationName, operation = null, resourceMessages = null) {
|
|
115
|
+
if (!operation || typeof operation !== "object" || Array.isArray(operation)) {
|
|
116
|
+
throw new TypeError(`defineResource operations.${operationName} must be an object.`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const source = normalizeObject(operation);
|
|
120
|
+
const normalizedOperation = {
|
|
121
|
+
...source
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (
|
|
125
|
+
resourceMessages &&
|
|
126
|
+
typeof resourceMessages === "object" &&
|
|
127
|
+
!Array.isArray(resourceMessages) &&
|
|
128
|
+
!Object.hasOwn(source, "messages")
|
|
129
|
+
) {
|
|
130
|
+
normalizedOperation.messages = resourceMessages;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const sectionName of OPERATION_SCHEMA_SECTION_NAMES) {
|
|
134
|
+
if (!Object.hasOwn(source, sectionName)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const defaultMode = resolveDefaultOperationSchemaMode(sectionName, source);
|
|
139
|
+
normalizedOperation[sectionName] = normalizeSchemaDefinitionLike(source[sectionName], {
|
|
140
|
+
context: `defineResource operations.${operationName}.${sectionName}`,
|
|
141
|
+
defaultMode
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return deepFreeze(normalizedOperation);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeResourceOperations(operations = null, resourceMessages = null) {
|
|
149
|
+
const source = normalizeObject(operations);
|
|
150
|
+
const normalizedOperations = {};
|
|
151
|
+
|
|
152
|
+
for (const [operationName, operation] of Object.entries(source)) {
|
|
153
|
+
const normalizedOperationName = normalizeText(operationName);
|
|
154
|
+
if (!normalizedOperationName) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
normalizedOperations[normalizedOperationName] = normalizeOperationDefinition(
|
|
159
|
+
normalizedOperationName,
|
|
160
|
+
operation,
|
|
161
|
+
resourceMessages
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return Object.freeze(normalizedOperations);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function requireResourceNamespace(value, {
|
|
169
|
+
context = "defineResource resource.namespace"
|
|
170
|
+
} = {}) {
|
|
171
|
+
const normalizedNamespace = normalizeText(value);
|
|
172
|
+
if (!normalizedNamespace) {
|
|
173
|
+
throw new TypeError(`${context} requires a non-empty namespace.`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return normalizedNamespace;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function defineResource(resource = {}) {
|
|
180
|
+
const source = normalizeObject(resource);
|
|
181
|
+
const normalizedMessages = Object.hasOwn(source, "messages")
|
|
182
|
+
? normalizeObject(source.messages)
|
|
183
|
+
: null;
|
|
184
|
+
|
|
185
|
+
return deepFreeze({
|
|
186
|
+
...source,
|
|
187
|
+
namespace: requireResourceNamespace(source.namespace),
|
|
188
|
+
...(normalizedMessages ? { messages: normalizedMessages } : {}),
|
|
189
|
+
operations: normalizeResourceOperations(source.operations, normalizedMessages)
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export {
|
|
194
|
+
createSchemaDefinition,
|
|
195
|
+
defineResource,
|
|
196
|
+
normalizeSchemaDefinitionLike
|
|
197
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createSchema } from "@jskit-ai/kernel/shared/validators";
|
|
4
|
+
import {
|
|
5
|
+
createSchemaDefinition,
|
|
6
|
+
defineResource,
|
|
7
|
+
normalizeSchemaDefinitionLike
|
|
8
|
+
} from "../src/shared/resource.js";
|
|
9
|
+
|
|
10
|
+
test("createSchemaDefinition wraps json-rest-schema instances with an explicit mode", () => {
|
|
11
|
+
const definition = createSchemaDefinition(createSchema({
|
|
12
|
+
ok: {
|
|
13
|
+
type: "boolean",
|
|
14
|
+
required: true
|
|
15
|
+
}
|
|
16
|
+
}), "replace");
|
|
17
|
+
|
|
18
|
+
assert.equal(typeof definition.schema.toJsonSchema, "function");
|
|
19
|
+
assert.equal(definition.mode, "replace");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("normalizeSchemaDefinitionLike accepts raw schemas and schema definition objects", () => {
|
|
23
|
+
const schema = createSchema({
|
|
24
|
+
name: {
|
|
25
|
+
type: "string",
|
|
26
|
+
required: true
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const fromSchema = normalizeSchemaDefinitionLike(schema, {
|
|
31
|
+
context: "test raw schema",
|
|
32
|
+
defaultMode: "create"
|
|
33
|
+
});
|
|
34
|
+
const fromDefinition = normalizeSchemaDefinitionLike({
|
|
35
|
+
schema,
|
|
36
|
+
mode: "patch"
|
|
37
|
+
}, {
|
|
38
|
+
context: "test schema definition"
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
assert.equal(fromSchema.mode, "create");
|
|
42
|
+
assert.equal(fromDefinition.mode, "patch");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("defineResource normalizes operation messages and schema sections", () => {
|
|
46
|
+
const resource = defineResource({
|
|
47
|
+
namespace: "assistantConfig",
|
|
48
|
+
messages: {
|
|
49
|
+
validation: "Fix invalid values."
|
|
50
|
+
},
|
|
51
|
+
operations: {
|
|
52
|
+
view: {
|
|
53
|
+
method: "GET",
|
|
54
|
+
output: createSchema({
|
|
55
|
+
ok: {
|
|
56
|
+
type: "boolean",
|
|
57
|
+
required: true
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
},
|
|
61
|
+
patch: {
|
|
62
|
+
method: "PATCH",
|
|
63
|
+
body: createSchema({
|
|
64
|
+
name: {
|
|
65
|
+
type: "string",
|
|
66
|
+
required: false
|
|
67
|
+
}
|
|
68
|
+
}),
|
|
69
|
+
output: createSchema({
|
|
70
|
+
ok: {
|
|
71
|
+
type: "boolean",
|
|
72
|
+
required: true
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
assert.equal(resource.namespace, "assistantConfig");
|
|
80
|
+
assert.equal(resource.operations.view.output.mode, "replace");
|
|
81
|
+
assert.equal(resource.operations.patch.body.mode, "patch");
|
|
82
|
+
assert.equal(resource.operations.patch.messages, resource.messages);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("defineResource rejects invalid operation schema section values eagerly", () => {
|
|
86
|
+
assert.throws(() => defineResource({
|
|
87
|
+
namespace: "assistantConfig",
|
|
88
|
+
operations: {
|
|
89
|
+
view: {
|
|
90
|
+
method: "GET",
|
|
91
|
+
output: true
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}), /operations\.view\.output must be a json-rest-schema schema instance or schema definition object/);
|
|
95
|
+
|
|
96
|
+
assert.throws(() => defineResource({
|
|
97
|
+
namespace: "assistantConfig",
|
|
98
|
+
operations: {
|
|
99
|
+
patch: {
|
|
100
|
+
method: "PATCH",
|
|
101
|
+
body: "invalid"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}), /operations\.patch\.body must be a json-rest-schema schema instance or schema definition object/);
|
|
105
|
+
});
|