@contentful/experience-design-system-types 2.39.1 → 2.40.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/package.json +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +4 -0
- package/dist/src/property-contract/date-time-value.d.ts +2 -0
- package/dist/src/property-contract/date-time-value.js +6 -0
- package/dist/src/property-contract/discriminated-union.d.ts +8 -0
- package/dist/src/property-contract/discriminated-union.js +52 -0
- package/dist/src/property-contract/index.d.ts +4 -0
- package/dist/src/property-contract/index.js +9 -0
- package/dist/src/property-contract/json-value.d.ts +16 -0
- package/dist/src/property-contract/json-value.js +145 -0
- package/dist/src/property-contract/target-definition.d.ts +166 -0
- package/dist/src/property-contract/target-definition.js +246 -0
- package/package.json +1 -1
package/dist/package.json
CHANGED
package/dist/src/index.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { validateCDF, parseCDFComponents, CDF_SCHEMA_URL, cdfJsonSchema, CDF_PRO
|
|
|
2
2
|
export { DESIGN_TOKEN_TYPES, flattenDTCG, validateDTCG, DTCGTokenSchema, type DesignTokenType, type DTCGTokenEntry, type DTCGTokenGroup, type DTCGTokenNode, type DTCGTokenGroupNode, type DTCGValidationError, type DTCGValidationResult, } from './dtcg/index.js';
|
|
3
3
|
export type { BreakingChange, ChangeClassification, DownstreamImpact, PropertySummary, ComponentTypeSummary, DesignTokenSummary, TaxonomySummary, ChangedEntity, EntityDiffGroup, ServerPreviewResponse, } from './sources-api/preview/index.js';
|
|
4
4
|
export type { ApplyOperationStatus, ApplyOperationItemError, ApplyOperationItem, ApplyOperationResponse, ApplyGateError, } from './sources-api/apply/index.js';
|
|
5
|
+
export * from './property-contract/index.js';
|
package/dist/src/index.js
CHANGED
|
@@ -3,3 +3,7 @@
|
|
|
3
3
|
export { validateCDF, parseCDFComponents, CDF_SCHEMA_URL, cdfJsonSchema, CDF_PROPERTY_TYPES, CDF_PROPERTY_CATEGORIES, CDFComponentSchema, CDFPropertySchema, CDFSlotSchema, CDFTokenSchema, buildCDF, buildFilteredCDF, stripUnsupportedSlotFields, validateSlotReferences, } from './cdf/index.js';
|
|
4
4
|
// DTCG (W3C Design Token Community Group)
|
|
5
5
|
export { DESIGN_TOKEN_TYPES, flattenDTCG, validateDTCG, DTCGTokenSchema, } from './dtcg/index.js';
|
|
6
|
+
// Property contract — pillar #4 foundation (target types + validators).
|
|
7
|
+
// Dormant: no current consumer imports these. Available for forthcoming
|
|
8
|
+
// evidence-aware extractors and generation. See INTEG-5147 / INTEG-4927.
|
|
9
|
+
export * from './property-contract/index.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import * as z from 'zod/mini';
|
|
2
|
+
const dateTimeValueSchema = z.iso.datetime({ offset: true });
|
|
3
|
+
/** Preserve timezone-qualified ISO datetime strings without coercion or normalization. */
|
|
4
|
+
export function isDateTimeValue(value) {
|
|
5
|
+
return dateTimeValueSchema.safeParse(value).success;
|
|
6
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type PropertyIssue } from './json-value.js';
|
|
2
|
+
type DefinitionStyle = 'target' | 'cdf';
|
|
3
|
+
type Definition = Record<string, unknown>;
|
|
4
|
+
/** An intrinsic tag belongs to one own field of a complete Record member. */
|
|
5
|
+
export declare function discriminatedUnionTag(member: unknown, discriminator: string, style: DefinitionStyle): Definition | undefined;
|
|
6
|
+
export declare function discriminatedUnionIssues(definition: Definition, style: DefinitionStyle, path?: string): PropertyIssue[];
|
|
7
|
+
export declare function matchingDiscriminatedUnionMember(definition: Definition, value: Definition, style: DefinitionStyle): Definition | undefined;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { childPath, jsonEqual, own } from './json-value.js';
|
|
2
|
+
function object(value) {
|
|
3
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
/** An intrinsic tag belongs to one own field of a complete Record member. */
|
|
6
|
+
export function discriminatedUnionTag(member, discriminator, style) {
|
|
7
|
+
if (!object(member) || member[style === 'target' ? 'type' : '$type'] !== (style === 'target' ? 'Record' : 'record'))
|
|
8
|
+
return;
|
|
9
|
+
const fields = member[style === 'target' ? 'fields' : '$fields'];
|
|
10
|
+
if (!object(fields) || !own(fields, discriminator))
|
|
11
|
+
return;
|
|
12
|
+
const field = fields[discriminator];
|
|
13
|
+
return object(field) ? field : undefined;
|
|
14
|
+
}
|
|
15
|
+
export function discriminatedUnionIssues(definition, style, path = '') {
|
|
16
|
+
const issues = [];
|
|
17
|
+
const discriminator = definition.discriminator;
|
|
18
|
+
if (typeof discriminator !== 'string' || !discriminator.length)
|
|
19
|
+
issues.push({ path: childPath(path, 'discriminator'), message: 'Union requires a nonempty discriminator' });
|
|
20
|
+
if (!Array.isArray(definition.members) || definition.members.length < 2) {
|
|
21
|
+
issues.push({ path: childPath(path, 'members'), message: 'Union requires at least two complete Record members' });
|
|
22
|
+
return issues;
|
|
23
|
+
}
|
|
24
|
+
const tags = new Set();
|
|
25
|
+
definition.members.forEach((member, index) => {
|
|
26
|
+
const at = childPath(childPath(path, 'members'), index);
|
|
27
|
+
const tag = typeof discriminator === 'string' ? discriminatedUnionTag(member, discriminator, style) : undefined;
|
|
28
|
+
if (!tag ||
|
|
29
|
+
tag[style === 'target' ? 'type' : '$type'] !== 'Literal' ||
|
|
30
|
+
tag[style === 'target' ? 'required' : '$required'] !== true) {
|
|
31
|
+
issues.push({ path: at, message: 'Each member requires one required intrinsic Literal discriminator' });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const value = tag.value;
|
|
35
|
+
if (!(typeof value === 'string' ||
|
|
36
|
+
typeof value === 'boolean' ||
|
|
37
|
+
(typeof value === 'number' && Number.isFinite(value)))) {
|
|
38
|
+
issues.push({ path: at, message: 'Discriminator must be a finite string, number or boolean literal' });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const key = JSON.stringify([typeof value, value]);
|
|
42
|
+
if (tags.has(key))
|
|
43
|
+
issues.push({ path: at, message: 'Union tags must be distinct typed values' });
|
|
44
|
+
tags.add(key);
|
|
45
|
+
});
|
|
46
|
+
return issues;
|
|
47
|
+
}
|
|
48
|
+
export function matchingDiscriminatedUnionMember(definition, value, style) {
|
|
49
|
+
if (discriminatedUnionIssues(definition, style).length || !own(value, definition.discriminator))
|
|
50
|
+
return;
|
|
51
|
+
return definition.members.find((member) => jsonEqual(discriminatedUnionTag(member, definition.discriminator, style).value, value[definition.discriminator]));
|
|
52
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Pillar #4 foundation — property-contract target types + validators.
|
|
2
|
+
// Dormant on `development` until downstream consumers (new extractors,
|
|
3
|
+
// generation, review UI) activate them. Shape is deliberately verbatim
|
|
4
|
+
// with the upstream work on feat/import-pipeline-evidence-and-reasoning
|
|
5
|
+
// so the two merge without drift. See INTEG-5147 (spike) / INTEG-4927.
|
|
6
|
+
export * from './json-value.js';
|
|
7
|
+
export * from './date-time-value.js';
|
|
8
|
+
export * from './discriminated-union.js';
|
|
9
|
+
export * from './target-definition.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | {
|
|
2
|
+
[key: string]: JsonValue;
|
|
3
|
+
};
|
|
4
|
+
export interface PropertyIssue {
|
|
5
|
+
path: string;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const own: (value: object, key: PropertyKey) => boolean;
|
|
9
|
+
export declare const childPath: (path: string, key: string | number) => string;
|
|
10
|
+
/** Inspect descriptors before reading values; aliases are allowed, cycles are not. */
|
|
11
|
+
export declare function jsonValueIssue(value: unknown, path?: string, ancestors?: Set<object>): PropertyIssue | undefined;
|
|
12
|
+
export declare function jsonEqual(left: unknown, right: unknown): boolean;
|
|
13
|
+
export declare function cloneJson<T>(value: T): T;
|
|
14
|
+
export declare function freezeJson<T>(value: T): Readonly<T>;
|
|
15
|
+
/** Parse the entire document and reject duplicate decoded own keys. */
|
|
16
|
+
export declare function parseStrictJson(text: string): unknown;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
export const own = (value, key) => Object.hasOwn(value, key);
|
|
2
|
+
export const childPath = (path, key) => `${path}/${String(key).replace(/~/g, '~0').replace(/\//g, '~1')}`;
|
|
3
|
+
/** Inspect descriptors before reading values; aliases are allowed, cycles are not. */
|
|
4
|
+
export function jsonValueIssue(value, path = '', ancestors = new Set()) {
|
|
5
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
6
|
+
return;
|
|
7
|
+
if (typeof value === 'number')
|
|
8
|
+
return Number.isFinite(value) && (!Number.isInteger(value) || Number.isSafeInteger(value))
|
|
9
|
+
? undefined
|
|
10
|
+
: { path, message: 'Expected finite JSON-safe number' };
|
|
11
|
+
if (typeof value !== 'object')
|
|
12
|
+
return { path, message: 'Expected JSON value' };
|
|
13
|
+
if (nodeTypes?.isProxy(value))
|
|
14
|
+
return { path, message: 'Proxy is not supported JSON data' };
|
|
15
|
+
if (ancestors.has(value))
|
|
16
|
+
return { path, message: 'Cyclic JSON value' };
|
|
17
|
+
const array = Array.isArray(value);
|
|
18
|
+
const proto = Object.getPrototypeOf(value);
|
|
19
|
+
if (array && proto !== Array.prototype)
|
|
20
|
+
return { path, message: 'Custom array prototype' };
|
|
21
|
+
if (!array && proto !== Object.prototype && proto !== null)
|
|
22
|
+
return { path, message: 'Custom object prototype' };
|
|
23
|
+
ancestors.add(value);
|
|
24
|
+
try {
|
|
25
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
26
|
+
if (array && key === 'length')
|
|
27
|
+
continue;
|
|
28
|
+
if (typeof key !== 'string')
|
|
29
|
+
return { path, message: 'Symbol key is not JSON' };
|
|
30
|
+
if (array && (!/^(0|[1-9]\d*)$/.test(key) || Number(key) >= value.length))
|
|
31
|
+
return { path: childPath(path, key), message: 'Extra array member' };
|
|
32
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
33
|
+
if (!descriptor.enumerable || !own(descriptor, 'value'))
|
|
34
|
+
return { path: childPath(path, key), message: 'Expected enumerable data member' };
|
|
35
|
+
const issue = jsonValueIssue(descriptor.value, childPath(path, key), ancestors);
|
|
36
|
+
if (issue)
|
|
37
|
+
return issue;
|
|
38
|
+
}
|
|
39
|
+
if (array)
|
|
40
|
+
for (let i = 0; i < value.length; i++)
|
|
41
|
+
if (!own(value, i))
|
|
42
|
+
return { path: childPath(path, i), message: 'Sparse array' };
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
ancestors.delete(value);
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
export function jsonEqual(left, right) {
|
|
50
|
+
if (Object.is(left, right) || left === right)
|
|
51
|
+
return true;
|
|
52
|
+
if (typeof left !== 'object' || left === null || typeof right !== 'object' || right === null)
|
|
53
|
+
return false;
|
|
54
|
+
if (Array.isArray(left) !== Array.isArray(right))
|
|
55
|
+
return false;
|
|
56
|
+
const keys = Object.keys(left).sort();
|
|
57
|
+
const other = Object.keys(right).sort();
|
|
58
|
+
return (keys.length === other.length &&
|
|
59
|
+
keys.every((key, i) => key === other[i] && jsonEqual(left[key], right[key])));
|
|
60
|
+
}
|
|
61
|
+
export function cloneJson(value) {
|
|
62
|
+
const issue = jsonValueIssue(value);
|
|
63
|
+
if (issue)
|
|
64
|
+
throw new Error(`${issue.path}: ${issue.message}`);
|
|
65
|
+
return JSON.parse(JSON.stringify(value));
|
|
66
|
+
}
|
|
67
|
+
export function freezeJson(value) {
|
|
68
|
+
if (value && typeof value === 'object') {
|
|
69
|
+
for (const item of Object.values(value))
|
|
70
|
+
freezeJson(item);
|
|
71
|
+
Object.freeze(value);
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
const nodeTypes = globalThis.process?.getBuiltinModule?.('node:util')?.types;
|
|
76
|
+
/** Parse the entire document and reject duplicate decoded own keys. */
|
|
77
|
+
export function parseStrictJson(text) {
|
|
78
|
+
const parsed = JSON.parse(text);
|
|
79
|
+
let cursor = 0;
|
|
80
|
+
const whitespace = () => {
|
|
81
|
+
while (/\s/.test(text[cursor] ?? '') && cursor < text.length)
|
|
82
|
+
cursor++;
|
|
83
|
+
};
|
|
84
|
+
const string = () => {
|
|
85
|
+
const start = cursor++;
|
|
86
|
+
while (cursor < text.length) {
|
|
87
|
+
const char = text[cursor++];
|
|
88
|
+
if (char === '\\')
|
|
89
|
+
cursor++;
|
|
90
|
+
else if (char === '"')
|
|
91
|
+
return JSON.parse(text.slice(start, cursor));
|
|
92
|
+
}
|
|
93
|
+
throw new Error('Unterminated JSON string');
|
|
94
|
+
};
|
|
95
|
+
const value = () => {
|
|
96
|
+
whitespace();
|
|
97
|
+
if (text[cursor] === '{') {
|
|
98
|
+
cursor++;
|
|
99
|
+
whitespace();
|
|
100
|
+
const keys = new Set();
|
|
101
|
+
if (text[cursor] === '}') {
|
|
102
|
+
cursor++;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
while (cursor < text.length) {
|
|
106
|
+
whitespace();
|
|
107
|
+
const key = string();
|
|
108
|
+
if (keys.has(key))
|
|
109
|
+
throw new Error('Duplicate JSON object key');
|
|
110
|
+
keys.add(key);
|
|
111
|
+
whitespace();
|
|
112
|
+
cursor++;
|
|
113
|
+
value();
|
|
114
|
+
whitespace();
|
|
115
|
+
if (text[cursor++] === '}')
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (text[cursor] === '[') {
|
|
120
|
+
cursor++;
|
|
121
|
+
whitespace();
|
|
122
|
+
if (text[cursor] === ']') {
|
|
123
|
+
cursor++;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
while (cursor < text.length) {
|
|
127
|
+
value();
|
|
128
|
+
whitespace();
|
|
129
|
+
if (text[cursor++] === ']')
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (text[cursor] === '"')
|
|
134
|
+
string();
|
|
135
|
+
else {
|
|
136
|
+
while (cursor < text.length && !/[\s,}\]]/.test(text[cursor]))
|
|
137
|
+
cursor++;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
value();
|
|
141
|
+
const issue = jsonValueIssue(parsed);
|
|
142
|
+
if (issue)
|
|
143
|
+
throw new Error(`${issue.path}: ${issue.message}`);
|
|
144
|
+
return parsed;
|
|
145
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { type JsonValue, type PropertyIssue } from './json-value.js';
|
|
2
|
+
export declare const TARGET_PROPERTY_TYPES: readonly ["String", "Number", "Boolean", "Array", "Record", "Integer", "DateTime", "RichText", "JSON", "ResourceLink", "Literal", "DiscriminatedUnion"];
|
|
3
|
+
export type TargetPropertyType = (typeof TARGET_PROPERTY_TYPES)[number];
|
|
4
|
+
export declare const SAME_SPACE_RESOURCE_SOURCE = "crn:contentful:::content:spaces/$self/environments/$self";
|
|
5
|
+
export type AllowedResourceDefinition = {
|
|
6
|
+
type: 'Contentful:Entry';
|
|
7
|
+
source: typeof SAME_SPACE_RESOURCE_SOURCE;
|
|
8
|
+
allowedTypes: string[];
|
|
9
|
+
} | {
|
|
10
|
+
type: 'Contentful:Asset';
|
|
11
|
+
source: typeof SAME_SPACE_RESOURCE_SOURCE;
|
|
12
|
+
};
|
|
13
|
+
export interface TargetDefinition {
|
|
14
|
+
type: TargetPropertyType;
|
|
15
|
+
id?: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
values?: JsonValue[];
|
|
20
|
+
defaultValue?: JsonValue;
|
|
21
|
+
items?: TargetDefinition;
|
|
22
|
+
fields?: Record<string, TargetDefinition>;
|
|
23
|
+
allowedResources?: AllowedResourceDefinition[];
|
|
24
|
+
value?: JsonValue;
|
|
25
|
+
valueType?: TargetDefinition;
|
|
26
|
+
discriminator?: string;
|
|
27
|
+
members?: TargetDefinition[];
|
|
28
|
+
}
|
|
29
|
+
export interface DefinitionValidation {
|
|
30
|
+
valid: boolean;
|
|
31
|
+
issues: PropertyIssue[];
|
|
32
|
+
}
|
|
33
|
+
export declare function containsTargetDefinitionType(definition: TargetDefinition, type: TargetPropertyType): boolean;
|
|
34
|
+
export declare function validateTargetValue(definition: TargetDefinition, value: JsonValue, path?: string): PropertyIssue[];
|
|
35
|
+
export declare function validateTargetDefinition(input: unknown): DefinitionValidation;
|
|
36
|
+
/** Portable structure only; runtime validation checks default/domain semantics. */
|
|
37
|
+
export declare const targetDefinitionJsonSchema: {
|
|
38
|
+
readonly $schema: "http://json-schema.org/draft-07/schema#";
|
|
39
|
+
readonly $ref: "#/definitions/target";
|
|
40
|
+
readonly definitions: {
|
|
41
|
+
readonly target: {
|
|
42
|
+
readonly type: "object";
|
|
43
|
+
readonly required: readonly ["type"];
|
|
44
|
+
readonly properties: {
|
|
45
|
+
readonly type: {
|
|
46
|
+
readonly enum: readonly ["String", "Number", "Boolean", "Array", "Record", "Integer", "DateTime", "RichText", "JSON", "ResourceLink", "Literal", "DiscriminatedUnion"];
|
|
47
|
+
};
|
|
48
|
+
readonly id: {
|
|
49
|
+
readonly type: "string";
|
|
50
|
+
};
|
|
51
|
+
readonly name: {
|
|
52
|
+
readonly type: "string";
|
|
53
|
+
};
|
|
54
|
+
readonly description: {
|
|
55
|
+
readonly type: "string";
|
|
56
|
+
};
|
|
57
|
+
readonly required: {
|
|
58
|
+
readonly type: "boolean";
|
|
59
|
+
};
|
|
60
|
+
readonly values: {
|
|
61
|
+
readonly type: "array";
|
|
62
|
+
readonly minItems: 1;
|
|
63
|
+
};
|
|
64
|
+
readonly defaultValue: {};
|
|
65
|
+
readonly items: {
|
|
66
|
+
readonly $ref: "#/definitions/target";
|
|
67
|
+
};
|
|
68
|
+
readonly fields: {
|
|
69
|
+
readonly type: "object";
|
|
70
|
+
readonly minProperties: 1;
|
|
71
|
+
readonly additionalProperties: {
|
|
72
|
+
readonly $ref: "#/definitions/target";
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
readonly allowedResources: {
|
|
76
|
+
readonly type: "array";
|
|
77
|
+
readonly minItems: 1;
|
|
78
|
+
readonly items: {
|
|
79
|
+
readonly type: "object";
|
|
80
|
+
readonly required: readonly ["type", "source"];
|
|
81
|
+
readonly properties: {
|
|
82
|
+
readonly type: {
|
|
83
|
+
readonly enum: readonly ["Contentful:Entry", "Contentful:Asset"];
|
|
84
|
+
};
|
|
85
|
+
readonly source: {
|
|
86
|
+
readonly const: "crn:contentful:::content:spaces/$self/environments/$self";
|
|
87
|
+
};
|
|
88
|
+
readonly allowedTypes: {
|
|
89
|
+
readonly type: "array";
|
|
90
|
+
readonly items: {
|
|
91
|
+
readonly type: "string";
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
readonly allOf: readonly [{
|
|
96
|
+
readonly if: {
|
|
97
|
+
readonly properties: {
|
|
98
|
+
readonly type: {
|
|
99
|
+
readonly const: "Contentful:Entry";
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
readonly then: {
|
|
104
|
+
readonly required: readonly ["allowedTypes"];
|
|
105
|
+
};
|
|
106
|
+
readonly else: {
|
|
107
|
+
readonly not: {
|
|
108
|
+
readonly required: readonly ["allowedTypes"];
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
}];
|
|
112
|
+
readonly additionalProperties: false;
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
readonly value: {};
|
|
116
|
+
readonly valueType: {
|
|
117
|
+
readonly $ref: "#/definitions/target";
|
|
118
|
+
};
|
|
119
|
+
readonly discriminator: {
|
|
120
|
+
readonly type: "string";
|
|
121
|
+
readonly minLength: 1;
|
|
122
|
+
};
|
|
123
|
+
readonly members: {
|
|
124
|
+
readonly type: "array";
|
|
125
|
+
readonly minItems: 2;
|
|
126
|
+
readonly uniqueItems: true;
|
|
127
|
+
readonly items: {
|
|
128
|
+
readonly allOf: readonly [{
|
|
129
|
+
readonly $ref: "#/definitions/target";
|
|
130
|
+
}, {
|
|
131
|
+
readonly type: "object";
|
|
132
|
+
readonly properties: {
|
|
133
|
+
readonly type: {
|
|
134
|
+
readonly const: "Record";
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
readonly required: readonly ["type", "fields"];
|
|
138
|
+
}];
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
readonly allOf: readonly [{
|
|
143
|
+
readonly if: {
|
|
144
|
+
readonly properties: {
|
|
145
|
+
readonly type: {
|
|
146
|
+
readonly const: "DiscriminatedUnion";
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
readonly then: {
|
|
151
|
+
readonly required: readonly ["discriminator", "members"];
|
|
152
|
+
};
|
|
153
|
+
readonly else: {
|
|
154
|
+
readonly not: {
|
|
155
|
+
readonly anyOf: readonly [{
|
|
156
|
+
readonly required: readonly ["discriminator"];
|
|
157
|
+
}, {
|
|
158
|
+
readonly required: readonly ["members"];
|
|
159
|
+
}];
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
}];
|
|
163
|
+
readonly additionalProperties: false;
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { childPath, jsonEqual, jsonValueIssue, own } from './json-value.js';
|
|
2
|
+
import { isDateTimeValue } from './date-time-value.js';
|
|
3
|
+
import { discriminatedUnionIssues, matchingDiscriminatedUnionMember } from './discriminated-union.js';
|
|
4
|
+
export const TARGET_PROPERTY_TYPES = [
|
|
5
|
+
'String',
|
|
6
|
+
'Number',
|
|
7
|
+
'Boolean',
|
|
8
|
+
'Array',
|
|
9
|
+
'Record',
|
|
10
|
+
'Integer',
|
|
11
|
+
'DateTime',
|
|
12
|
+
'RichText',
|
|
13
|
+
'JSON',
|
|
14
|
+
'ResourceLink',
|
|
15
|
+
'Literal',
|
|
16
|
+
'DiscriminatedUnion',
|
|
17
|
+
];
|
|
18
|
+
export const SAME_SPACE_RESOURCE_SOURCE = 'crn:contentful:::content:spaces/$self/environments/$self';
|
|
19
|
+
export function containsTargetDefinitionType(definition, type) {
|
|
20
|
+
return (definition.type === type ||
|
|
21
|
+
(definition.items !== undefined && containsTargetDefinitionType(definition.items, type)) ||
|
|
22
|
+
(definition.valueType !== undefined && containsTargetDefinitionType(definition.valueType, type)) ||
|
|
23
|
+
Object.values(definition.fields ?? {}).some((field) => containsTargetDefinitionType(field, type)) ||
|
|
24
|
+
(definition.members ?? []).some((member) => containsTargetDefinitionType(member, type)));
|
|
25
|
+
}
|
|
26
|
+
export function validateTargetValue(definition, value, path = '') {
|
|
27
|
+
const fail = (message) => [{ path, message }];
|
|
28
|
+
const { type } = definition;
|
|
29
|
+
if (definition.values && !definition.values.some((allowed) => jsonEqual(allowed, value)))
|
|
30
|
+
return fail('Value outside declared domain');
|
|
31
|
+
if (type === 'String' || type === 'DateTime') {
|
|
32
|
+
if (typeof value !== 'string')
|
|
33
|
+
return fail('Expected string');
|
|
34
|
+
if (type === 'DateTime' && !isDateTimeValue(value))
|
|
35
|
+
return fail('Expected timezone-qualified ISO datetime');
|
|
36
|
+
}
|
|
37
|
+
else if (type === 'Number' || type === 'Integer') {
|
|
38
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || (type === 'Integer' && !Number.isSafeInteger(value)))
|
|
39
|
+
return fail(type === 'Integer' ? 'Expected safe integer' : 'Expected finite number');
|
|
40
|
+
}
|
|
41
|
+
else if (type === 'Boolean') {
|
|
42
|
+
if (typeof value !== 'boolean')
|
|
43
|
+
return fail('Expected boolean');
|
|
44
|
+
}
|
|
45
|
+
else if (type === 'Array') {
|
|
46
|
+
if (!Array.isArray(value))
|
|
47
|
+
return fail('Expected array');
|
|
48
|
+
return value.flatMap((item, i) => validateTargetValue(definition.items, item, childPath(path, i)));
|
|
49
|
+
}
|
|
50
|
+
else if (type === 'Record') {
|
|
51
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
52
|
+
return fail('Expected record');
|
|
53
|
+
const issues = [];
|
|
54
|
+
for (const key of Object.keys(value))
|
|
55
|
+
if (!own(definition.fields, key))
|
|
56
|
+
issues.push({ path: childPath(path, key), message: 'Unknown field' });
|
|
57
|
+
for (const [key, field] of Object.entries(definition.fields)) {
|
|
58
|
+
if (!own(value, key)) {
|
|
59
|
+
if (field.required)
|
|
60
|
+
issues.push({ path: childPath(path, key), message: 'Required field missing' });
|
|
61
|
+
}
|
|
62
|
+
else
|
|
63
|
+
issues.push(...validateTargetValue(field, value[key], childPath(path, key)));
|
|
64
|
+
}
|
|
65
|
+
return issues;
|
|
66
|
+
}
|
|
67
|
+
else if (type === 'DiscriminatedUnion') {
|
|
68
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
69
|
+
return fail('Expected union record');
|
|
70
|
+
const member = matchingDiscriminatedUnionMember(definition, value, 'target');
|
|
71
|
+
return member
|
|
72
|
+
? validateTargetValue(member, value, path)
|
|
73
|
+
: fail('Union value requires an exact matching discriminator tag');
|
|
74
|
+
}
|
|
75
|
+
else if (type === 'Literal') {
|
|
76
|
+
if (!jsonEqual(value, definition.value))
|
|
77
|
+
return fail('Default must equal intrinsic literal');
|
|
78
|
+
return validateTargetValue(definition.valueType, value, path);
|
|
79
|
+
}
|
|
80
|
+
if (definition.values && !definition.values.some((allowed) => jsonEqual(allowed, value)))
|
|
81
|
+
return fail('Value outside declared domain');
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
export function validateTargetDefinition(input) {
|
|
85
|
+
const jsonIssue = jsonValueIssue(input);
|
|
86
|
+
if (jsonIssue)
|
|
87
|
+
return { valid: false, issues: [jsonIssue] };
|
|
88
|
+
const issues = [];
|
|
89
|
+
const walk = (value, path, depth) => {
|
|
90
|
+
if (depth > 64) {
|
|
91
|
+
issues.push({ path, message: 'Definition depth exceeds 64' });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
95
|
+
issues.push({ path, message: 'Expected target definition' });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const d = value;
|
|
99
|
+
const add = (key, message) => issues.push({ path: childPath(path, key), message });
|
|
100
|
+
if (!TARGET_PROPERTY_TYPES.includes(d.type)) {
|
|
101
|
+
add('type', 'Unknown target type');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const allowed = new Set(['type', 'id', 'name', 'description', 'required', 'values', 'defaultValue']);
|
|
105
|
+
if (d.type === 'Array')
|
|
106
|
+
allowed.add('items');
|
|
107
|
+
if (d.type === 'Record')
|
|
108
|
+
allowed.add('fields');
|
|
109
|
+
if (d.type === 'DiscriminatedUnion') {
|
|
110
|
+
allowed.add('discriminator');
|
|
111
|
+
allowed.add('members');
|
|
112
|
+
}
|
|
113
|
+
if (d.type === 'ResourceLink')
|
|
114
|
+
allowed.add('allowedResources');
|
|
115
|
+
if (d.type === 'Literal') {
|
|
116
|
+
allowed.add('value');
|
|
117
|
+
allowed.add('valueType');
|
|
118
|
+
}
|
|
119
|
+
for (const key of Object.keys(d))
|
|
120
|
+
if (!allowed.has(key))
|
|
121
|
+
add(key, 'Unexpected target metadata');
|
|
122
|
+
for (const key of ['id', 'name', 'description'])
|
|
123
|
+
if (own(d, key) && typeof d[key] !== 'string')
|
|
124
|
+
add(key, 'Expected string');
|
|
125
|
+
if (own(d, 'required') && typeof d.required !== 'boolean')
|
|
126
|
+
add('required', 'Expected boolean');
|
|
127
|
+
const before = issues.length;
|
|
128
|
+
if (d.type === 'DiscriminatedUnion') {
|
|
129
|
+
issues.push(...discriminatedUnionIssues(d, 'target', path));
|
|
130
|
+
if (Array.isArray(d.members))
|
|
131
|
+
d.members.forEach((member, index) => walk(member, childPath(childPath(path, 'members'), index), depth + 1));
|
|
132
|
+
}
|
|
133
|
+
if (d.type === 'Array')
|
|
134
|
+
walk(d.items, childPath(path, 'items'), depth + 1);
|
|
135
|
+
if (d.type === 'Record') {
|
|
136
|
+
if (!d.fields || typeof d.fields !== 'object' || Array.isArray(d.fields) || Object.keys(d.fields).length === 0)
|
|
137
|
+
add('fields', 'Expected nonempty field dictionary');
|
|
138
|
+
else
|
|
139
|
+
for (const [key, field] of Object.entries(d.fields))
|
|
140
|
+
walk(field, childPath(childPath(path, 'fields'), key), depth + 1);
|
|
141
|
+
}
|
|
142
|
+
if (d.type === 'ResourceLink') {
|
|
143
|
+
if (!Array.isArray(d.allowedResources) || d.allowedResources.length === 0)
|
|
144
|
+
add('allowedResources', 'Expected same-space Entry/Asset resources');
|
|
145
|
+
else
|
|
146
|
+
d.allowedResources.forEach((r, i) => {
|
|
147
|
+
if (!r ||
|
|
148
|
+
!['Contentful:Entry', 'Contentful:Asset'].includes(r.type) ||
|
|
149
|
+
r.source !== SAME_SPACE_RESOURCE_SOURCE ||
|
|
150
|
+
Object.keys(r).some((key) => !['type', 'source', ...(r.type === 'Contentful:Entry' ? ['allowedTypes'] : [])].includes(key)) ||
|
|
151
|
+
(r.type === 'Contentful:Entry' &&
|
|
152
|
+
(!Array.isArray(r.allowedTypes) || !r.allowedTypes.every((type) => typeof type === 'string'))))
|
|
153
|
+
add(`allowedResources/${i}`, 'Expected same-space Entry/Asset resource');
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (d.type === 'Literal') {
|
|
157
|
+
if (!own(d, 'value'))
|
|
158
|
+
add('value', 'Intrinsic literal missing');
|
|
159
|
+
walk(d.valueType, childPath(path, 'valueType'), depth + 1);
|
|
160
|
+
}
|
|
161
|
+
if (own(d, 'values') && (!Array.isArray(d.values) || d.values.length === 0))
|
|
162
|
+
add('values', 'Expected nonempty declared domain');
|
|
163
|
+
if (issues.length !== before)
|
|
164
|
+
return;
|
|
165
|
+
if (d.values) {
|
|
166
|
+
const withoutDomain = { ...d };
|
|
167
|
+
delete withoutDomain.values;
|
|
168
|
+
d.values.forEach((v, i) => issues.push(...validateTargetValue(withoutDomain, v, childPath(childPath(path, 'values'), i))));
|
|
169
|
+
}
|
|
170
|
+
if (d.type === 'Literal' && own(d, 'value'))
|
|
171
|
+
issues.push(...validateTargetValue(d.valueType, d.value, childPath(path, 'value')));
|
|
172
|
+
if (own(d, 'defaultValue')) {
|
|
173
|
+
if (d.type === 'RichText' || d.type === 'JSON')
|
|
174
|
+
add('defaultValue', 'Target does not support defaults');
|
|
175
|
+
else
|
|
176
|
+
issues.push(...validateTargetValue(d, d.defaultValue, childPath(path, 'defaultValue')));
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
walk(input, '', 0);
|
|
180
|
+
return { valid: issues.length === 0, issues };
|
|
181
|
+
}
|
|
182
|
+
/** Portable structure only; runtime validation checks default/domain semantics. */
|
|
183
|
+
export const targetDefinitionJsonSchema = {
|
|
184
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
185
|
+
$ref: '#/definitions/target',
|
|
186
|
+
definitions: {
|
|
187
|
+
target: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
required: ['type'],
|
|
190
|
+
properties: {
|
|
191
|
+
type: { enum: [...TARGET_PROPERTY_TYPES] },
|
|
192
|
+
id: { type: 'string' },
|
|
193
|
+
name: { type: 'string' },
|
|
194
|
+
description: { type: 'string' },
|
|
195
|
+
required: { type: 'boolean' },
|
|
196
|
+
values: { type: 'array', minItems: 1 },
|
|
197
|
+
defaultValue: {},
|
|
198
|
+
items: { $ref: '#/definitions/target' },
|
|
199
|
+
fields: { type: 'object', minProperties: 1, additionalProperties: { $ref: '#/definitions/target' } },
|
|
200
|
+
allowedResources: {
|
|
201
|
+
type: 'array',
|
|
202
|
+
minItems: 1,
|
|
203
|
+
items: {
|
|
204
|
+
type: 'object',
|
|
205
|
+
required: ['type', 'source'],
|
|
206
|
+
properties: {
|
|
207
|
+
type: { enum: ['Contentful:Entry', 'Contentful:Asset'] },
|
|
208
|
+
source: { const: SAME_SPACE_RESOURCE_SOURCE },
|
|
209
|
+
allowedTypes: { type: 'array', items: { type: 'string' } },
|
|
210
|
+
},
|
|
211
|
+
allOf: [
|
|
212
|
+
{
|
|
213
|
+
if: { properties: { type: { const: 'Contentful:Entry' } } },
|
|
214
|
+
then: { required: ['allowedTypes'] },
|
|
215
|
+
else: { not: { required: ['allowedTypes'] } },
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
additionalProperties: false,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
value: {},
|
|
222
|
+
valueType: { $ref: '#/definitions/target' },
|
|
223
|
+
discriminator: { type: 'string', minLength: 1 },
|
|
224
|
+
members: {
|
|
225
|
+
type: 'array',
|
|
226
|
+
minItems: 2,
|
|
227
|
+
uniqueItems: true,
|
|
228
|
+
items: {
|
|
229
|
+
allOf: [
|
|
230
|
+
{ $ref: '#/definitions/target' },
|
|
231
|
+
{ type: 'object', properties: { type: { const: 'Record' } }, required: ['type', 'fields'] },
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
allOf: [
|
|
237
|
+
{
|
|
238
|
+
if: { properties: { type: { const: 'DiscriminatedUnion' } } },
|
|
239
|
+
then: { required: ['discriminator', 'members'] },
|
|
240
|
+
else: { not: { anyOf: [{ required: ['discriminator'] }, { required: ['members'] }] } },
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
additionalProperties: false,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
};
|