@pplethai/cli 0.6.0 → 0.8.0
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/dist/bin.js +171 -22
- package/package.json +3 -2
- package/templates/golden-path/.dev.vars.example +19 -1
- package/templates/golden-path/CLAUDE.md +74 -7
- package/templates/golden-path/README.md +27 -2
- package/templates/golden-path/gitignore +3 -0
- package/templates/golden-path/overlay-mtime.mjs +30 -0
- package/templates/golden-path/package-lock.json +19 -1
- package/templates/golden-path/package.json +3 -2
- package/templates/golden-path/scripts/assert-dev-compiled-out.mjs +42 -0
- package/templates/golden-path/shared/api.ts +42 -2
- package/templates/golden-path/src/auth.ts +99 -7
- package/templates/golden-path/src/call-service.ts +140 -0
- package/templates/golden-path/src/dataset-query.ts +470 -0
- package/templates/golden-path/src/dataset-schema.ts +398 -0
- package/templates/golden-path/src/dev-call-service.ts +391 -0
- package/templates/golden-path/src/dev-personas.ts +105 -7
- package/templates/golden-path/src/dev-read-dataset.ts +429 -0
- package/templates/golden-path/src/dev-registry.ts +276 -0
- package/templates/golden-path/src/env.ts +17 -0
- package/templates/golden-path/src/identity-header.ts +17 -0
- package/templates/golden-path/src/index.ts +32 -1
- package/templates/golden-path/src/legal-identity.ts +180 -0
- package/templates/golden-path/src/levels.ts +132 -0
- package/templates/golden-path/src/read-dataset.ts +376 -0
- package/templates/golden-path/src/registry.ts +122 -0
- package/templates/golden-path/src/vite-env.d.ts +13 -0
- package/templates/golden-path/test/call-service.test.ts +178 -0
- package/templates/golden-path/test/dev-call-service.test.ts +279 -0
- package/templates/golden-path/test/dev-read-dataset.test.ts +270 -0
- package/templates/golden-path/test/dev-registry.test.ts +375 -0
- package/templates/golden-path/test/effective-sub.test.ts +101 -0
- package/templates/golden-path/test/fixtures/overlay/directory/branches/entry.md +85 -0
- package/templates/golden-path/test/fixtures/overlay/directory/branches/mocks/audience.json +3 -0
- package/templates/golden-path/test/fixtures/overlay/directory/branches/mocks/named.json +3 -0
- package/templates/golden-path/test/fixtures/overlay/directory/branches/mocks/offices.json +6 -0
- package/templates/golden-path/test/fixtures/overlay/directory/branches/mocks/roster.json +3 -0
- package/templates/golden-path/test/fixtures/overlay/directory/branches/mocks/summary.json +1 -0
- package/templates/golden-path/test/fixtures/overlay/platform/legal-identity/entry.md +8 -0
- package/templates/golden-path/test/fixtures/overlay/platform/legal-identity/mocks/responses.json +13 -0
- package/templates/golden-path/test/fixtures/overlay/shop/catalog/entry.md +8 -0
- package/templates/golden-path/test/fixtures/overlay/shop/catalog/mocks/responses.json +7 -0
- package/templates/golden-path/test/fixtures/overlay/shop/orders/entry.md +12 -0
- package/templates/golden-path/test/fixtures/overlay/shop/orders/mocks/responses.json +36 -0
- package/templates/golden-path/test/harness.ts +101 -16
- package/templates/golden-path/test/legal-identity.test.ts +323 -0
- package/templates/golden-path/test/membership.test.ts +105 -0
- package/templates/golden-path/test/read-dataset.test.ts +214 -0
- package/templates/golden-path/test/service-call.test.ts +166 -0
- package/templates/golden-path/vite.config.ts +2 -1
- package/templates/golden-path/vitest.config.ts +2 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// A copy of workers/src/shared/registry/schema.ts.
|
|
2
|
+
//
|
|
3
|
+
// The template is copied into a Builder App, which cannot import the platform
|
|
4
|
+
// package. dataset-query.ts uses canonicalJson, isPlainObject and
|
|
5
|
+
// valueProblems from this copy so a filter value is accepted or refused the
|
|
6
|
+
// same way offline as in the warehouse.
|
|
7
|
+
|
|
8
|
+
// The JSON Schema subset a Dataset Object Type may declare (ADR-0027).
|
|
9
|
+
// Anything outside this list is refused, so a Provider cannot smuggle a
|
|
10
|
+
// keyword the sweep would not enforce. The root is an object. Nested
|
|
11
|
+
// schemas use the same list. `null` is allowed only inside a type list.
|
|
12
|
+
|
|
13
|
+
const SCHEMA_KEYS = new Set([
|
|
14
|
+
"type",
|
|
15
|
+
"properties",
|
|
16
|
+
"required",
|
|
17
|
+
"items",
|
|
18
|
+
"enum",
|
|
19
|
+
"format",
|
|
20
|
+
"minimum",
|
|
21
|
+
"maximum",
|
|
22
|
+
"maxLength",
|
|
23
|
+
"description",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const TYPE_NAMES = new Set(["string", "number", "integer", "boolean", "object", "array", "null"]);
|
|
27
|
+
const FORMATS = new Set(["date", "date-time", "email", "uri"]);
|
|
28
|
+
const SCALAR_TYPES = new Set(["string", "number", "integer", "boolean"]);
|
|
29
|
+
|
|
30
|
+
/** How deep a nested schema may go. A sweep page is a document, not a tree. */
|
|
31
|
+
const MAX_DEPTH = 32;
|
|
32
|
+
|
|
33
|
+
const KEY_LIST =
|
|
34
|
+
"type, properties, required, items, enum, format, minimum, maximum, maxLength และ description";
|
|
35
|
+
|
|
36
|
+
export function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
37
|
+
return (
|
|
38
|
+
value !== null &&
|
|
39
|
+
typeof value === "object" &&
|
|
40
|
+
!Array.isArray(value) &&
|
|
41
|
+
Object.getPrototypeOf(value) === Object.prototype
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Stable JSON: object keys sorted, array order kept. Used to see whether a schema changed. */
|
|
46
|
+
export function canonicalJson(value: unknown): string {
|
|
47
|
+
return JSON.stringify(canonicalize(value));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function canonicalize(value: unknown): unknown {
|
|
51
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
52
|
+
if (!isPlainObject(value)) return value;
|
|
53
|
+
const out: Record<string, unknown> = {};
|
|
54
|
+
for (const key of Object.keys(value).sort()) out[key] = canonicalize(value[key]);
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Every problem in one schema, each a Thai sentence. `root` is the Object
|
|
60
|
+
* Type's schema, which must be an object. An empty list means the schema is
|
|
61
|
+
* in the subset.
|
|
62
|
+
*/
|
|
63
|
+
export function schemaProblems(schema: unknown, root = true): string[] {
|
|
64
|
+
return walkSchema(schema, "", root, 0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function walkSchema(schema: unknown, path: string, root: boolean, depth: number): string[] {
|
|
68
|
+
const where = path === "" ? "schema" : `schema ที่ ${path}`;
|
|
69
|
+
if (!isPlainObject(schema)) return [`${where} ต้องเป็นอ็อบเจกต์`];
|
|
70
|
+
if (depth > MAX_DEPTH) return [`${where} ซ้อนกันเกิน ${MAX_DEPTH} ชั้น`];
|
|
71
|
+
|
|
72
|
+
const problems: string[] = [];
|
|
73
|
+
for (const key of Object.keys(schema).filter((key) => !SCHEMA_KEYS.has(key)).sort()) {
|
|
74
|
+
problems.push(`${where} ใช้ "${key}" ไม่ได้ — ใช้ได้เฉพาะ ${KEY_LIST}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const types = readTypes(schema.type);
|
|
78
|
+
if (!types.ok) {
|
|
79
|
+
problems.push(
|
|
80
|
+
`${where}: type ต้องเป็น string, number, integer, boolean, object, array, null หรือรายการของชนิดเหล่านี้`,
|
|
81
|
+
);
|
|
82
|
+
} else if (root && (types.names.length !== 1 || types.names[0] !== "object")) {
|
|
83
|
+
problems.push(`${where} ที่รากต้องมี type เป็น object`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!types.ok) return problems;
|
|
87
|
+
const names = new Set(types.names);
|
|
88
|
+
const has = (type: string) => names.has(type);
|
|
89
|
+
|
|
90
|
+
if ("properties" in schema) {
|
|
91
|
+
if (!has("object")) {
|
|
92
|
+
problems.push(`${where}: properties ใช้ได้เมื่อ type เป็น object`);
|
|
93
|
+
} else if (!isPlainObject(schema.properties)) {
|
|
94
|
+
problems.push(`${where}: properties ต้องเป็นชื่อฟิลด์และ schema`);
|
|
95
|
+
} else {
|
|
96
|
+
for (const key of Object.keys(schema.properties).sort()) {
|
|
97
|
+
if (key === "") problems.push(`${where}: ชื่อฟิลด์ใน properties ต้องไม่ว่าง`);
|
|
98
|
+
const child = path === "" ? key : `${path}.${key}`;
|
|
99
|
+
problems.push(...walkSchema(schema.properties[key], child, false, depth + 1));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} else if (has("object") && root) {
|
|
103
|
+
// A root object may declare no fields. Nested objects may too.
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if ("required" in schema) {
|
|
107
|
+
if (!has("object")) {
|
|
108
|
+
problems.push(`${where}: required ใช้ได้เมื่อ type เป็น object`);
|
|
109
|
+
} else if (
|
|
110
|
+
!Array.isArray(schema.required) ||
|
|
111
|
+
schema.required.some((item) => typeof item !== "string" || item === "")
|
|
112
|
+
) {
|
|
113
|
+
problems.push(`${where}: required ต้องเป็นรายชื่อฟิลด์`);
|
|
114
|
+
} else if (!isPlainObject(schema.properties)) {
|
|
115
|
+
problems.push(`${where}: required ต้องอ้างฟิลด์ที่ประกาศใน properties`);
|
|
116
|
+
} else {
|
|
117
|
+
const seen = new Set<string>();
|
|
118
|
+
for (const name of schema.required) {
|
|
119
|
+
if (seen.has(name)) problems.push(`${where}: required ระบุ "${name}" ซ้ำ`);
|
|
120
|
+
seen.add(name);
|
|
121
|
+
if (!Object.hasOwn(schema.properties, name)) {
|
|
122
|
+
problems.push(`${where}: required อ้าง "${name}" ที่ไม่มีใน properties`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if ("items" in schema) {
|
|
129
|
+
if (!has("array")) {
|
|
130
|
+
problems.push(`${where}: items ใช้ได้เมื่อ type เป็น array`);
|
|
131
|
+
} else if (Array.isArray(schema.items)) {
|
|
132
|
+
problems.push(`${where}: items ต้องเป็น schema เดียว`);
|
|
133
|
+
} else {
|
|
134
|
+
const child = path === "" ? "items" : `${path}.items`;
|
|
135
|
+
problems.push(...walkSchema(schema.items, child, false, depth + 1));
|
|
136
|
+
}
|
|
137
|
+
} else if (has("array")) {
|
|
138
|
+
problems.push(`${where}: type เป็น array ต้องมี items`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if ("enum" in schema) {
|
|
142
|
+
if (!Array.isArray(schema.enum) || schema.enum.length === 0) {
|
|
143
|
+
problems.push(`${where}: enum ต้องมีอย่างน้อย 1 ค่า`);
|
|
144
|
+
} else {
|
|
145
|
+
schema.enum.forEach((item, index) => {
|
|
146
|
+
const fit = valueProblems(schema, item);
|
|
147
|
+
if (fit.length > 0) {
|
|
148
|
+
problems.push(`${where}: ค่าที่ ${index + 1} ใน enum ไม่ตรงกับ schema`);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if ("format" in schema) {
|
|
155
|
+
if (!has("string")) {
|
|
156
|
+
problems.push(`${where}: format ใช้ได้เมื่อ type เป็น string`);
|
|
157
|
+
} else if (typeof schema.format !== "string" || !FORMATS.has(schema.format)) {
|
|
158
|
+
problems.push(`${where}: format ต้องเป็น date, date-time, email หรือ uri`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if ("maxLength" in schema) {
|
|
163
|
+
if (!has("string")) {
|
|
164
|
+
problems.push(`${where}: maxLength ใช้ได้เมื่อ type เป็น string`);
|
|
165
|
+
} else if (!nonNegativeInteger(schema.maxLength)) {
|
|
166
|
+
problems.push(`${where}: maxLength ต้องเป็นจำนวนเต็มที่ไม่ติดลบ`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const hasNumber = has("number") || has("integer");
|
|
171
|
+
if ("minimum" in schema || "maximum" in schema) {
|
|
172
|
+
if (!hasNumber) {
|
|
173
|
+
problems.push(`${where}: minimum และ maximum ใช้ได้เมื่อ type เป็น number หรือ integer`);
|
|
174
|
+
} else {
|
|
175
|
+
if ("minimum" in schema && !finiteNumber(schema.minimum)) {
|
|
176
|
+
problems.push(`${where}: minimum ต้องเป็นจำนวน`);
|
|
177
|
+
}
|
|
178
|
+
if ("maximum" in schema && !finiteNumber(schema.maximum)) {
|
|
179
|
+
problems.push(`${where}: maximum ต้องเป็นจำนวน`);
|
|
180
|
+
}
|
|
181
|
+
if (
|
|
182
|
+
finiteNumber(schema.minimum) &&
|
|
183
|
+
finiteNumber(schema.maximum) &&
|
|
184
|
+
schema.minimum > schema.maximum
|
|
185
|
+
) {
|
|
186
|
+
problems.push(`${where}: minimum ต้องไม่มากกว่า maximum`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if ("description" in schema && typeof schema.description !== "string") {
|
|
192
|
+
problems.push(`${where}: description ต้องเป็นข้อความ`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return problems;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Whether one JSON value fits a schema the subset already accepted. Each
|
|
200
|
+
* problem is a Thai sentence about the value, not the schema. An empty list
|
|
201
|
+
* means it fits. Extra properties are kept: the sweep drops them later.
|
|
202
|
+
*/
|
|
203
|
+
export function valueProblems(schema: unknown, value: unknown): string[] {
|
|
204
|
+
if (!isPlainObject(schema)) return ["ค่าไม่ตรงกับ schema"];
|
|
205
|
+
const types = readTypes(schema.type);
|
|
206
|
+
if (!types.ok) return ["ค่าไม่ตรงกับ schema"];
|
|
207
|
+
return valueWalk(schema, types.names, value);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* A copy of `value` with every property the schema does not declare removed,
|
|
212
|
+
* at every depth. The names are dotted paths (`address.note`), each once.
|
|
213
|
+
* Array items share their array's path, so the same undeclared field on
|
|
214
|
+
* every item is one name. The input is not changed.
|
|
215
|
+
*/
|
|
216
|
+
export function dropUndeclared(schema: unknown, value: unknown): { value: unknown; dropped: string[] } {
|
|
217
|
+
const dropped: string[] = [];
|
|
218
|
+
const next = dropWalk(schema, value, "", dropped);
|
|
219
|
+
return { value: next, dropped: [...new Set(dropped)].sort() };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function dropWalk(schema: unknown, value: unknown, path: string, dropped: string[]): unknown {
|
|
223
|
+
if (!isPlainObject(schema) || value === null) return value;
|
|
224
|
+
const types = readTypes(schema.type);
|
|
225
|
+
if (!types.ok) return value;
|
|
226
|
+
if (types.names.includes("object") && isPlainObject(value)) {
|
|
227
|
+
const properties = isPlainObject(schema.properties) ? schema.properties : {};
|
|
228
|
+
const out: Record<string, unknown> = {};
|
|
229
|
+
for (const key of Object.keys(value)) {
|
|
230
|
+
const child = path === "" ? key : `${path}.${key}`;
|
|
231
|
+
// Own keys only: \`in\` would also match Object.prototype names such as
|
|
232
|
+
// "constructor" or "toString", and keep an undeclared field.
|
|
233
|
+
if (!Object.hasOwn(properties, key)) {
|
|
234
|
+
dropped.push(child);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
// defineProperty, so a declared "__proto__" stays an own field.
|
|
238
|
+
Object.defineProperty(out, key, {
|
|
239
|
+
value: dropWalk(properties[key], value[key], child, dropped),
|
|
240
|
+
enumerable: true,
|
|
241
|
+
writable: true,
|
|
242
|
+
configurable: true,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
if (types.names.includes("array") && Array.isArray(value) && isPlainObject(schema.items)) {
|
|
248
|
+
return value.map((item) => dropWalk(schema.items, item, path, dropped));
|
|
249
|
+
}
|
|
250
|
+
return value;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function valueWalk(schema: Record<string, unknown>, types: readonly string[], value: unknown): string[] {
|
|
254
|
+
if (value === null) {
|
|
255
|
+
if (!types.includes("null")) return ["ต้องไม่เป็น null"];
|
|
256
|
+
if ("enum" in schema && Array.isArray(schema.enum) && !schema.enum.some((item) => item === null)) {
|
|
257
|
+
return ["ต้องเป็นหนึ่งในค่าที่กำหนด"];
|
|
258
|
+
}
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
const matched = types.filter((type) => type !== "null" && valueIs(type, value));
|
|
262
|
+
if (matched.length === 0) return [expected(types)];
|
|
263
|
+
|
|
264
|
+
const problems: string[] = [];
|
|
265
|
+
if ("enum" in schema && Array.isArray(schema.enum) && !schema.enum.some((item) => sameValue(item, value))) {
|
|
266
|
+
problems.push("ต้องเป็นหนึ่งในค่าที่กำหนด");
|
|
267
|
+
}
|
|
268
|
+
if (matched.includes("string") && typeof value === "string") {
|
|
269
|
+
if (typeof schema.format === "string" && !formatFits(schema.format, value)) {
|
|
270
|
+
problems.push(`ต้องเป็นรูปแบบ ${schema.format}`);
|
|
271
|
+
}
|
|
272
|
+
if (typeof schema.maxLength === "number" && [...value].length > schema.maxLength) {
|
|
273
|
+
problems.push(`ยาวเกิน ${schema.maxLength} ตัวอักษร`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if ((matched.includes("number") || matched.includes("integer")) && typeof value === "number") {
|
|
277
|
+
if (typeof schema.minimum === "number" && value < schema.minimum) {
|
|
278
|
+
problems.push(`ต้องไม่น้อยกว่า ${schema.minimum}`);
|
|
279
|
+
}
|
|
280
|
+
if (typeof schema.maximum === "number" && value > schema.maximum) {
|
|
281
|
+
problems.push(`ต้องไม่เกิน ${schema.maximum}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (matched.includes("object") && isPlainObject(value)) {
|
|
285
|
+
const properties = isPlainObject(schema.properties) ? schema.properties : {};
|
|
286
|
+
const required = Array.isArray(schema.required)
|
|
287
|
+
? schema.required.filter((item): item is string => typeof item === "string")
|
|
288
|
+
: [];
|
|
289
|
+
for (const key of required) {
|
|
290
|
+
if (!Object.hasOwn(value, key)) problems.push(`ต้องมี "${key}"`);
|
|
291
|
+
}
|
|
292
|
+
for (const key of Object.keys(properties).sort()) {
|
|
293
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
294
|
+
const child = properties[key];
|
|
295
|
+
if (!isPlainObject(child)) continue;
|
|
296
|
+
const nested = valueProblems(child, value[key]);
|
|
297
|
+
for (const problem of nested) problems.push(`${key}: ${problem}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (matched.includes("array") && Array.isArray(value) && isPlainObject(schema.items)) {
|
|
301
|
+
value.forEach((item, index) => {
|
|
302
|
+
for (const problem of valueProblems(schema.items, item)) {
|
|
303
|
+
problems.push(`รายการที่ ${index + 1}: ${problem}`);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return problems;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function expected(types: readonly string[]): string {
|
|
311
|
+
const names = types.filter((type) => type !== "null");
|
|
312
|
+
if (names.length === 1) {
|
|
313
|
+
if (names[0] === "string") return "ต้องเป็นข้อความ";
|
|
314
|
+
if (names[0] === "integer") return "ต้องเป็นจำนวนเต็ม";
|
|
315
|
+
if (names[0] === "number") return "ต้องเป็นจำนวน";
|
|
316
|
+
if (names[0] === "boolean") return "ต้องเป็นจริงหรือเท็จ";
|
|
317
|
+
if (names[0] === "object") return "ต้องเป็นอ็อบเจกต์";
|
|
318
|
+
if (names[0] === "array") return "ต้องเป็นรายการ";
|
|
319
|
+
}
|
|
320
|
+
return "ต้องตรงกับ type ที่ประกาศ";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function valueIs(type: string, value: unknown): boolean {
|
|
324
|
+
if (type === "string") return typeof value === "string";
|
|
325
|
+
if (type === "boolean") return typeof value === "boolean";
|
|
326
|
+
if (type === "integer") return typeof value === "number" && Number.isInteger(value);
|
|
327
|
+
if (type === "number") return typeof value === "number" && Number.isFinite(value);
|
|
328
|
+
if (type === "object") return isPlainObject(value);
|
|
329
|
+
if (type === "array") return Array.isArray(value);
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function formatFits(format: string, value: string): boolean {
|
|
334
|
+
if (format === "date") return isDate(value);
|
|
335
|
+
if (format === "date-time") return isDateTime(value);
|
|
336
|
+
if (format === "email") return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
337
|
+
if (format === "uri") {
|
|
338
|
+
try {
|
|
339
|
+
return new URL(value).protocol.length > 1;
|
|
340
|
+
} catch {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function isDate(value: string): boolean {
|
|
348
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
349
|
+
if (!match) return false;
|
|
350
|
+
const year = Number(match[1]);
|
|
351
|
+
const month = Number(match[2]);
|
|
352
|
+
const day = Number(match[3]);
|
|
353
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
354
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function isDateTime(value: string): boolean {
|
|
358
|
+
const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+)?(Z|[+-]\d{2}:\d{2})$/.exec(value);
|
|
359
|
+
if (!match) return false;
|
|
360
|
+
const hour = Number(match[2]);
|
|
361
|
+
const minute = Number(match[3]);
|
|
362
|
+
const second = Number(match[4]);
|
|
363
|
+
if (hour > 23 || minute > 59 || second > 60) return false;
|
|
364
|
+
return isDate(match[1] ?? "");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function sameValue(left: unknown, right: unknown): boolean {
|
|
368
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function readTypes(value: unknown): { ok: true; names: string[] } | { ok: false } {
|
|
372
|
+
const list = typeof value === "string" ? [value] : Array.isArray(value) ? value : null;
|
|
373
|
+
if (!list || list.length === 0) return { ok: false };
|
|
374
|
+
if (!list.every((item) => typeof item === "string" && TYPE_NAMES.has(item))) return { ok: false };
|
|
375
|
+
if (new Set(list).size !== list.length) return { ok: false };
|
|
376
|
+
return { ok: true, names: list };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function finiteNumber(value: unknown): value is number {
|
|
380
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function nonNegativeInteger(value: unknown): boolean {
|
|
384
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** A top-level property an `index` may name: a scalar, or a scalar that may be null. */
|
|
388
|
+
export function isScalarSchema(schema: unknown): boolean {
|
|
389
|
+
if (!isPlainObject(schema)) return false;
|
|
390
|
+
const types = readTypes(schema.type);
|
|
391
|
+
if (!types.ok) return false;
|
|
392
|
+
return types.names.every((type) => type === "null" || SCALAR_TYPES.has(type));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** The id property: exactly string or integer, never null and never a list. */
|
|
396
|
+
export function isIdSchema(schema: unknown): boolean {
|
|
397
|
+
return isPlainObject(schema) && (schema.type === "string" || schema.type === "integer");
|
|
398
|
+
}
|