@ontrails/core 1.0.0-beta.3 → 1.0.0-beta.30
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/CHANGELOG.md +305 -8
- package/README.md +114 -33
- package/package.json +11 -1
- package/src/activation-provenance.ts +111 -0
- package/src/activation-source-compatibility.ts +430 -0
- package/src/activation-source-projection.ts +221 -0
- package/src/activation-source.ts +91 -0
- package/src/blob-ref.ts +51 -0
- package/src/branded.ts +1 -1
- package/src/compose-batch.ts +69 -0
- package/src/compose-schema.ts +36 -0
- package/src/context.ts +44 -10
- package/src/contour.ts +346 -0
- package/src/derive.ts +241 -16
- package/src/detours.ts +8 -0
- package/src/draft.ts +342 -0
- package/src/error-projection.ts +51 -0
- package/src/errors.ts +335 -48
- package/src/execute.ts +1577 -0
- package/src/fire.ts +1169 -0
- package/src/index.ts +495 -34
- package/src/internal/fork-ctx.ts +69 -0
- package/src/layer-projection.ts +193 -0
- package/src/layer.ts +43 -6
- package/src/observe.ts +361 -0
- package/src/path-security.ts +1 -1
- package/src/permits.ts +12 -0
- package/src/redaction/patterns.ts +6 -3
- package/src/resilience.ts +11 -11
- package/src/resource-config.ts +792 -0
- package/src/resource.ts +194 -0
- package/src/result.ts +18 -0
- package/src/run.ts +76 -0
- package/src/schedule-runtime.ts +689 -0
- package/src/schedule.ts +326 -0
- package/src/serialization.ts +92 -55
- package/src/signal-diagnostics.ts +633 -0
- package/src/signal-ref.ts +87 -0
- package/src/signal.ts +104 -0
- package/src/store/accessor-protocol.ts +56 -0
- package/src/store/index.ts +4 -0
- package/src/structured-examples.ts +248 -0
- package/src/surface-derivation.ts +91 -0
- package/src/surface-filter.ts +176 -0
- package/src/surface-versioning.ts +42 -0
- package/src/topo.ts +769 -67
- package/src/tracing.ts +345 -0
- package/src/trail.ts +1168 -30
- package/src/trails/derive-trail.ts +842 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +139 -0
- package/src/trails-db.ts +206 -0
- package/src/transport-error-map.ts +174 -0
- package/src/type-utils.ts +87 -0
- package/src/types.ts +254 -12
- package/src/validate-established-topo.ts +69 -0
- package/src/validate-topo.ts +393 -45
- package/src/validation.ts +152 -4
- package/src/version-marker.ts +716 -0
- package/src/version-resolution.ts +308 -0
- package/src/version-runtime.ts +120 -0
- package/src/webhook.ts +278 -0
- package/src/workspace.ts +1 -1
- package/src/zod-wrappers.ts +72 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/adapters.d.ts +0 -39
- package/dist/adapters.d.ts.map +0 -1
- package/dist/adapters.js +0 -2
- package/dist/adapters.js.map +0 -1
- package/dist/blob-ref.d.ts +0 -20
- package/dist/blob-ref.d.ts.map +0 -1
- package/dist/blob-ref.js +0 -22
- package/dist/blob-ref.js.map +0 -1
- package/dist/branded.d.ts +0 -36
- package/dist/branded.d.ts.map +0 -1
- package/dist/branded.js +0 -89
- package/dist/branded.js.map +0 -1
- package/dist/collections.d.ts +0 -31
- package/dist/collections.d.ts.map +0 -1
- package/dist/collections.js +0 -60
- package/dist/collections.js.map +0 -1
- package/dist/context.d.ts +0 -10
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -15
- package/dist/context.js.map +0 -1
- package/dist/derive.d.ts +0 -33
- package/dist/derive.d.ts.map +0 -1
- package/dist/derive.js +0 -125
- package/dist/derive.js.map +0 -1
- package/dist/errors.d.ts +0 -83
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -142
- package/dist/errors.js.map +0 -1
- package/dist/event.d.ts +0 -45
- package/dist/event.d.ts.map +0 -1
- package/dist/event.js +0 -17
- package/dist/event.js.map +0 -1
- package/dist/fetch.d.ts +0 -15
- package/dist/fetch.d.ts.map +0 -1
- package/dist/fetch.js +0 -102
- package/dist/fetch.js.map +0 -1
- package/dist/guards.d.ts +0 -17
- package/dist/guards.d.ts.map +0 -1
- package/dist/guards.js +0 -25
- package/dist/guards.js.map +0 -1
- package/dist/health.d.ts +0 -18
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js +0 -5
- package/dist/health.js.map +0 -1
- package/dist/hike.d.ts +0 -36
- package/dist/hike.d.ts.map +0 -1
- package/dist/hike.js +0 -20
- package/dist/hike.js.map +0 -1
- package/dist/index.d.ts +0 -36
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -40
- package/dist/index.js.map +0 -1
- package/dist/job.d.ts +0 -24
- package/dist/job.d.ts.map +0 -1
- package/dist/job.js +0 -17
- package/dist/job.js.map +0 -1
- package/dist/layer.d.ts +0 -17
- package/dist/layer.d.ts.map +0 -1
- package/dist/layer.js +0 -21
- package/dist/layer.js.map +0 -1
- package/dist/path-security.d.ts +0 -28
- package/dist/path-security.d.ts.map +0 -1
- package/dist/path-security.js +0 -63
- package/dist/path-security.js.map +0 -1
- package/dist/patterns/bulk.d.ts +0 -15
- package/dist/patterns/bulk.d.ts.map +0 -1
- package/dist/patterns/bulk.js +0 -14
- package/dist/patterns/bulk.js.map +0 -1
- package/dist/patterns/change.d.ts +0 -10
- package/dist/patterns/change.d.ts.map +0 -1
- package/dist/patterns/change.js +0 -10
- package/dist/patterns/change.js.map +0 -1
- package/dist/patterns/date-range.d.ts +0 -10
- package/dist/patterns/date-range.d.ts.map +0 -1
- package/dist/patterns/date-range.js +0 -10
- package/dist/patterns/date-range.js.map +0 -1
- package/dist/patterns/index.d.ts +0 -9
- package/dist/patterns/index.d.ts.map +0 -1
- package/dist/patterns/index.js +0 -9
- package/dist/patterns/index.js.map +0 -1
- package/dist/patterns/pagination.d.ts +0 -18
- package/dist/patterns/pagination.d.ts.map +0 -1
- package/dist/patterns/pagination.js +0 -18
- package/dist/patterns/pagination.js.map +0 -1
- package/dist/patterns/progress.d.ts +0 -11
- package/dist/patterns/progress.d.ts.map +0 -1
- package/dist/patterns/progress.js +0 -11
- package/dist/patterns/progress.js.map +0 -1
- package/dist/patterns/sorting.d.ts +0 -13
- package/dist/patterns/sorting.d.ts.map +0 -1
- package/dist/patterns/sorting.js +0 -10
- package/dist/patterns/sorting.js.map +0 -1
- package/dist/patterns/status.d.ts +0 -15
- package/dist/patterns/status.d.ts.map +0 -1
- package/dist/patterns/status.js +0 -9
- package/dist/patterns/status.js.map +0 -1
- package/dist/patterns/timestamps.d.ts +0 -10
- package/dist/patterns/timestamps.d.ts.map +0 -1
- package/dist/patterns/timestamps.js +0 -10
- package/dist/patterns/timestamps.js.map +0 -1
- package/dist/redaction/index.d.ts +0 -4
- package/dist/redaction/index.d.ts.map +0 -1
- package/dist/redaction/index.js +0 -3
- package/dist/redaction/index.js.map +0 -1
- package/dist/redaction/patterns.d.ts +0 -9
- package/dist/redaction/patterns.d.ts.map +0 -1
- package/dist/redaction/patterns.js +0 -39
- package/dist/redaction/patterns.js.map +0 -1
- package/dist/redaction/redactor.d.ts +0 -27
- package/dist/redaction/redactor.d.ts.map +0 -1
- package/dist/redaction/redactor.js +0 -89
- package/dist/redaction/redactor.js.map +0 -1
- package/dist/resilience.d.ts +0 -34
- package/dist/resilience.d.ts.map +0 -1
- package/dist/resilience.js +0 -164
- package/dist/resilience.js.map +0 -1
- package/dist/result.d.ts +0 -57
- package/dist/result.d.ts.map +0 -1
- package/dist/result.js +0 -156
- package/dist/result.js.map +0 -1
- package/dist/serialization.d.ts +0 -27
- package/dist/serialization.d.ts.map +0 -1
- package/dist/serialization.js +0 -153
- package/dist/serialization.js.map +0 -1
- package/dist/topo.d.ts +0 -18
- package/dist/topo.d.ts.map +0 -1
- package/dist/topo.js +0 -80
- package/dist/topo.js.map +0 -1
- package/dist/trail.d.ts +0 -83
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -16
- package/dist/trail.js.map +0 -1
- package/dist/types.d.ts +0 -46
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/validate-topo.d.ts +0 -24
- package/dist/validate-topo.d.ts.map +0 -1
- package/dist/validate-topo.js +0 -156
- package/dist/validate-topo.js.map +0 -1
- package/dist/validation.d.ts +0 -27
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -134
- package/dist/validation.js.map +0 -1
- package/dist/workspace.d.ts +0 -25
- package/dist/workspace.d.ts.map +0 -1
- package/dist/workspace.js +0 -57
- package/dist/workspace.js.map +0 -1
- package/src/__tests__/blob-ref.test.ts +0 -103
- package/src/__tests__/branded.test.ts +0 -148
- package/src/__tests__/collections.test.ts +0 -126
- package/src/__tests__/context.test.ts +0 -66
- package/src/__tests__/derive.test.ts +0 -203
- package/src/__tests__/errors.test.ts +0 -309
- package/src/__tests__/event.test.ts +0 -82
- package/src/__tests__/fetch.test.ts +0 -217
- package/src/__tests__/guards.test.ts +0 -102
- package/src/__tests__/hike.test.ts +0 -117
- package/src/__tests__/job.test.ts +0 -98
- package/src/__tests__/layer.test.ts +0 -224
- package/src/__tests__/path-security.test.ts +0 -114
- package/src/__tests__/patterns.test.ts +0 -273
- package/src/__tests__/redaction.test.ts +0 -244
- package/src/__tests__/resilience.test.ts +0 -246
- package/src/__tests__/result.test.ts +0 -155
- package/src/__tests__/serialization.test.ts +0 -401
- package/src/__tests__/topo.test.ts +0 -220
- package/src/__tests__/trail.test.ts +0 -179
- package/src/__tests__/validate-topo.test.ts +0 -280
- package/src/__tests__/validation.test.ts +0 -283
- package/src/__tests__/workspace.test.ts +0 -183
- package/src/adapters.ts +0 -68
- package/src/event.ts +0 -77
- package/src/health.ts +0 -23
- package/src/hike.ts +0 -77
- package/src/job.ts +0 -20
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActivationSourceKind,
|
|
3
|
+
ActivationSourceMeta,
|
|
4
|
+
} from './activation-source.js';
|
|
5
|
+
|
|
6
|
+
export const ACTIVATION_PROVENANCE_KEY =
|
|
7
|
+
'__trails_activation_provenance' as const;
|
|
8
|
+
|
|
9
|
+
export interface ActivationProvenanceSource {
|
|
10
|
+
readonly cron?: string | undefined;
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly kind: ActivationSourceKind;
|
|
13
|
+
readonly meta?: ActivationSourceMeta | undefined;
|
|
14
|
+
readonly producerTrailId?: string | undefined;
|
|
15
|
+
readonly timezone?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ActivationProvenance {
|
|
19
|
+
readonly fireId: string;
|
|
20
|
+
readonly parentFireId?: string | undefined;
|
|
21
|
+
readonly rootFireId: string;
|
|
22
|
+
readonly source: ActivationProvenanceSource;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ActivationProvenanceCarrier {
|
|
26
|
+
readonly activation?: ActivationProvenance | undefined;
|
|
27
|
+
readonly extensions?: Readonly<Record<string, unknown>> | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
|
|
31
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
32
|
+
|
|
33
|
+
const optionalString = (value: unknown): boolean =>
|
|
34
|
+
value === undefined || typeof value === 'string';
|
|
35
|
+
|
|
36
|
+
const isActivationProvenanceSource = (
|
|
37
|
+
value: unknown
|
|
38
|
+
): value is ActivationProvenanceSource =>
|
|
39
|
+
isObjectRecord(value) &&
|
|
40
|
+
typeof value['id'] === 'string' &&
|
|
41
|
+
typeof value['kind'] === 'string' &&
|
|
42
|
+
optionalString(value['cron']) &&
|
|
43
|
+
optionalString(value['producerTrailId']) &&
|
|
44
|
+
optionalString(value['timezone']) &&
|
|
45
|
+
(value['meta'] === undefined || isObjectRecord(value['meta']));
|
|
46
|
+
|
|
47
|
+
const isActivationProvenance = (
|
|
48
|
+
value: unknown
|
|
49
|
+
): value is ActivationProvenance =>
|
|
50
|
+
isObjectRecord(value) &&
|
|
51
|
+
typeof value['fireId'] === 'string' &&
|
|
52
|
+
optionalString(value['parentFireId']) &&
|
|
53
|
+
typeof value['rootFireId'] === 'string' &&
|
|
54
|
+
isActivationProvenanceSource(value['source']);
|
|
55
|
+
|
|
56
|
+
export const getActivationProvenance = (
|
|
57
|
+
ctx: ActivationProvenanceCarrier | undefined
|
|
58
|
+
): ActivationProvenance | undefined => {
|
|
59
|
+
if (isActivationProvenance(ctx?.activation)) {
|
|
60
|
+
return ctx.activation;
|
|
61
|
+
}
|
|
62
|
+
const fromExtensions = ctx?.extensions?.[ACTIVATION_PROVENANCE_KEY];
|
|
63
|
+
return isActivationProvenance(fromExtensions) ? fromExtensions : undefined;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const withActivationProvenance = <
|
|
67
|
+
TCtx extends {
|
|
68
|
+
readonly extensions?: Readonly<Record<string, unknown>> | undefined;
|
|
69
|
+
},
|
|
70
|
+
>(
|
|
71
|
+
ctx: TCtx,
|
|
72
|
+
activation: ActivationProvenance
|
|
73
|
+
): TCtx & { readonly activation: ActivationProvenance } => ({
|
|
74
|
+
...ctx,
|
|
75
|
+
activation,
|
|
76
|
+
extensions: {
|
|
77
|
+
...ctx.extensions,
|
|
78
|
+
[ACTIVATION_PROVENANCE_KEY]: activation,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const buildActivationProvenanceTraceAttrs = (
|
|
83
|
+
activation: ActivationProvenance | undefined
|
|
84
|
+
): Readonly<Record<string, unknown>> => {
|
|
85
|
+
if (activation === undefined) {
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const attrs: Record<string, unknown> = {
|
|
90
|
+
'trails.activation.fire_id': activation.fireId,
|
|
91
|
+
'trails.activation.root_fire_id': activation.rootFireId,
|
|
92
|
+
'trails.activation.source.id': activation.source.id,
|
|
93
|
+
'trails.activation.source.kind': activation.source.kind,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (activation.parentFireId !== undefined) {
|
|
97
|
+
attrs['trails.activation.parent_fire_id'] = activation.parentFireId;
|
|
98
|
+
}
|
|
99
|
+
if (activation.source.producerTrailId !== undefined) {
|
|
100
|
+
attrs['trails.activation.source.producer_trail.id'] =
|
|
101
|
+
activation.source.producerTrailId;
|
|
102
|
+
}
|
|
103
|
+
if (activation.source.cron !== undefined) {
|
|
104
|
+
attrs['trails.activation.source.cron'] = activation.source.cron;
|
|
105
|
+
}
|
|
106
|
+
if (activation.source.timezone !== undefined) {
|
|
107
|
+
attrs['trails.activation.source.timezone'] = activation.source.timezone;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return attrs;
|
|
111
|
+
};
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import type { ActivationSource } from './activation-source.js';
|
|
4
|
+
import type { AnySignal } from './signal.js';
|
|
5
|
+
import { zodToJsonSchema } from './validation.js';
|
|
6
|
+
|
|
7
|
+
export interface ActivationSchemaIssue {
|
|
8
|
+
readonly code: string;
|
|
9
|
+
readonly message: string;
|
|
10
|
+
readonly path: readonly (string | number)[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type SourcePayloadContract =
|
|
14
|
+
| {
|
|
15
|
+
readonly schema: z.ZodType<unknown>;
|
|
16
|
+
readonly type: 'schema';
|
|
17
|
+
}
|
|
18
|
+
| {
|
|
19
|
+
readonly type: 'value';
|
|
20
|
+
readonly value: unknown;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type JsonSchemaObject = Record<string, unknown>;
|
|
24
|
+
|
|
25
|
+
const EMPTY_SOURCE_INPUT = Object.freeze({});
|
|
26
|
+
|
|
27
|
+
const hasOwn = (value: object, key: string): boolean =>
|
|
28
|
+
Object.hasOwn(value, key);
|
|
29
|
+
|
|
30
|
+
const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
|
|
31
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
32
|
+
|
|
33
|
+
const isZodSchema = (value: unknown): value is z.ZodType<unknown> =>
|
|
34
|
+
isObjectRecord(value) &&
|
|
35
|
+
typeof value['safeParse'] === 'function' &&
|
|
36
|
+
isObjectRecord(value['_zod']);
|
|
37
|
+
|
|
38
|
+
const normalizeIssuePath = (
|
|
39
|
+
path: readonly unknown[]
|
|
40
|
+
): readonly (string | number)[] =>
|
|
41
|
+
Object.freeze(
|
|
42
|
+
path.map((segment) =>
|
|
43
|
+
typeof segment === 'string' || typeof segment === 'number'
|
|
44
|
+
? segment
|
|
45
|
+
: String(segment)
|
|
46
|
+
)
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const getActivationPayloadContract = (
|
|
50
|
+
source: ActivationSource,
|
|
51
|
+
signals: ReadonlyMap<string, AnySignal>
|
|
52
|
+
): SourcePayloadContract | undefined => {
|
|
53
|
+
if (source.kind === 'signal') {
|
|
54
|
+
const signal = signals.get(source.id);
|
|
55
|
+
return signal ? { schema: signal.payload, type: 'schema' } : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (source.kind === 'schedule' && !hasOwn(source, 'input')) {
|
|
59
|
+
return { type: 'value', value: EMPTY_SOURCE_INPUT };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (isZodSchema(source.parse)) {
|
|
63
|
+
return { schema: source.parse, type: 'schema' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (isObjectRecord(source.parse) && isZodSchema(source.parse['output'])) {
|
|
67
|
+
return { schema: source.parse['output'], type: 'schema' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isZodSchema(source.payload)) {
|
|
71
|
+
return { schema: source.payload, type: 'schema' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isZodSchema(source.input)) {
|
|
75
|
+
return { schema: source.input, type: 'schema' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (hasOwn(source, 'input')) {
|
|
79
|
+
return { type: 'value', value: source.input };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return undefined;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const zodIssuesToCompatibilityIssues = (
|
|
86
|
+
issues: readonly z.ZodIssue[]
|
|
87
|
+
): readonly ActivationSchemaIssue[] =>
|
|
88
|
+
Object.freeze(
|
|
89
|
+
issues.map((issue) => ({
|
|
90
|
+
code: issue.code,
|
|
91
|
+
message: issue.message,
|
|
92
|
+
path: normalizeIssuePath(issue.path),
|
|
93
|
+
}))
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const jsonSchemaFor = (schema: z.ZodType<unknown>): JsonSchemaObject =>
|
|
97
|
+
zodToJsonSchema(schema) as JsonSchemaObject;
|
|
98
|
+
|
|
99
|
+
const jsonObjectAt = (
|
|
100
|
+
schema: JsonSchemaObject,
|
|
101
|
+
key: string
|
|
102
|
+
): JsonSchemaObject | undefined => {
|
|
103
|
+
const value = schema[key];
|
|
104
|
+
return isObjectRecord(value) ? value : undefined;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const jsonObjectEntriesAt = (
|
|
108
|
+
schema: JsonSchemaObject,
|
|
109
|
+
key: string
|
|
110
|
+
): readonly [string, JsonSchemaObject][] => {
|
|
111
|
+
const value = schema[key];
|
|
112
|
+
if (!isObjectRecord(value)) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
return Object.entries(value).filter(
|
|
116
|
+
(entry): entry is [string, JsonSchemaObject] => isObjectRecord(entry[1])
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const jsonSchemaArrayAt = (
|
|
121
|
+
schema: JsonSchemaObject,
|
|
122
|
+
key: string
|
|
123
|
+
): readonly JsonSchemaObject[] => {
|
|
124
|
+
const value = schema[key];
|
|
125
|
+
if (!Array.isArray(value)) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
return value.filter(isObjectRecord);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const stringArrayAt = (
|
|
132
|
+
schema: JsonSchemaObject,
|
|
133
|
+
key: string
|
|
134
|
+
): readonly string[] => {
|
|
135
|
+
const value = schema[key];
|
|
136
|
+
return Array.isArray(value)
|
|
137
|
+
? value.filter((entry): entry is string => typeof entry === 'string')
|
|
138
|
+
: [];
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const primitiveTypeOf = (value: unknown): string => {
|
|
142
|
+
if (value === null) {
|
|
143
|
+
return 'null';
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(value)) {
|
|
146
|
+
return 'array';
|
|
147
|
+
}
|
|
148
|
+
return typeof value;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const schemaTypes = (
|
|
152
|
+
schema: JsonSchemaObject
|
|
153
|
+
): ReadonlySet<string> | undefined => {
|
|
154
|
+
const { type } = schema;
|
|
155
|
+
if (typeof type === 'string') {
|
|
156
|
+
return new Set([type]);
|
|
157
|
+
}
|
|
158
|
+
if (Array.isArray(type)) {
|
|
159
|
+
const types = type.filter(
|
|
160
|
+
(entry): entry is string => typeof entry === 'string'
|
|
161
|
+
);
|
|
162
|
+
return types.length > 0 ? new Set(types) : undefined;
|
|
163
|
+
}
|
|
164
|
+
if (hasOwn(schema, 'const')) {
|
|
165
|
+
return new Set([primitiveTypeOf(schema['const'])]);
|
|
166
|
+
}
|
|
167
|
+
const enumValues = schema['enum'];
|
|
168
|
+
if (Array.isArray(enumValues) && enumValues.length > 0) {
|
|
169
|
+
return new Set(enumValues.map(primitiveTypeOf));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const anyOf = jsonSchemaArrayAt(schema, 'anyOf');
|
|
173
|
+
if (anyOf.length > 0) {
|
|
174
|
+
const allTypes = new Set<string>();
|
|
175
|
+
for (const option of anyOf) {
|
|
176
|
+
const optionTypes = schemaTypes(option);
|
|
177
|
+
if (!optionTypes) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
for (const optionType of optionTypes) {
|
|
181
|
+
allTypes.add(optionType);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return allTypes;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return undefined;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const setsIntersect = (
|
|
191
|
+
left: ReadonlySet<string>,
|
|
192
|
+
right: ReadonlySet<string>
|
|
193
|
+
): boolean => {
|
|
194
|
+
for (const entry of left) {
|
|
195
|
+
if (right.has(entry)) {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const describeSchemaTypes = (types: ReadonlySet<string> | undefined): string =>
|
|
203
|
+
types ? [...types].toSorted().join(' | ') : 'unknown';
|
|
204
|
+
|
|
205
|
+
const valueSet = (
|
|
206
|
+
schema: JsonSchemaObject
|
|
207
|
+
): ReadonlySet<unknown> | undefined => {
|
|
208
|
+
if (hasOwn(schema, 'const')) {
|
|
209
|
+
return new Set([schema['const']]);
|
|
210
|
+
}
|
|
211
|
+
const enumValues = schema['enum'];
|
|
212
|
+
if (Array.isArray(enumValues)) {
|
|
213
|
+
return new Set(enumValues);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const anyOf = jsonSchemaArrayAt(schema, 'anyOf');
|
|
217
|
+
if (anyOf.length === 0) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const values = new Set<unknown>();
|
|
222
|
+
for (const option of anyOf) {
|
|
223
|
+
const optionValues = valueSet(option);
|
|
224
|
+
if (optionValues === undefined) {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
for (const value of optionValues) {
|
|
228
|
+
values.add(value);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return values;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const allValuesIn = (
|
|
235
|
+
source: ReadonlySet<unknown>,
|
|
236
|
+
target: ReadonlySet<unknown>
|
|
237
|
+
): boolean => {
|
|
238
|
+
for (const value of source) {
|
|
239
|
+
if (!target.has(value)) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return true;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const compatibilityIssue = (
|
|
247
|
+
code: string,
|
|
248
|
+
path: readonly (string | number)[],
|
|
249
|
+
message: string
|
|
250
|
+
): ActivationSchemaIssue => ({
|
|
251
|
+
code,
|
|
252
|
+
message,
|
|
253
|
+
path: Object.freeze([...path]),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const sourceGuaranteesObjectProperty = (
|
|
257
|
+
key: string,
|
|
258
|
+
required: ReadonlySet<string>,
|
|
259
|
+
properties: ReadonlyMap<string, JsonSchemaObject>
|
|
260
|
+
): boolean => {
|
|
261
|
+
const property = properties.get(key);
|
|
262
|
+
return (
|
|
263
|
+
property !== undefined && (required.has(key) || hasOwn(property, 'default'))
|
|
264
|
+
);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const checkEnumCompatibility = (
|
|
268
|
+
source: JsonSchemaObject,
|
|
269
|
+
target: JsonSchemaObject,
|
|
270
|
+
path: readonly (string | number)[]
|
|
271
|
+
): readonly ActivationSchemaIssue[] => {
|
|
272
|
+
if (!hasOwn(target, 'const') && !Array.isArray(target['enum'])) {
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const targetValues = valueSet(target);
|
|
277
|
+
const sourceValues = valueSet(source);
|
|
278
|
+
if (targetValues && sourceValues && allValuesIn(sourceValues, targetValues)) {
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return [
|
|
283
|
+
compatibilityIssue(
|
|
284
|
+
hasOwn(target, 'const') ? 'const' : 'enum',
|
|
285
|
+
path,
|
|
286
|
+
'Source schema can produce values outside the accepted input values'
|
|
287
|
+
),
|
|
288
|
+
];
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const checkTypeCompatibility = (
|
|
292
|
+
source: JsonSchemaObject,
|
|
293
|
+
target: JsonSchemaObject,
|
|
294
|
+
path: readonly (string | number)[]
|
|
295
|
+
): readonly ActivationSchemaIssue[] => {
|
|
296
|
+
const targetTypes = schemaTypes(target);
|
|
297
|
+
if (!targetTypes) {
|
|
298
|
+
return [];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const sourceTypes = schemaTypes(source);
|
|
302
|
+
if (sourceTypes && setsIntersect(sourceTypes, targetTypes)) {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return [
|
|
307
|
+
compatibilityIssue(
|
|
308
|
+
'type',
|
|
309
|
+
path,
|
|
310
|
+
`Source schema type ${describeSchemaTypes(sourceTypes)} is not compatible with input schema type ${describeSchemaTypes(targetTypes)}`
|
|
311
|
+
),
|
|
312
|
+
];
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const checkSchemaCompatibility = (
|
|
316
|
+
source: JsonSchemaObject,
|
|
317
|
+
target: JsonSchemaObject,
|
|
318
|
+
path: readonly (string | number)[] = Object.freeze([])
|
|
319
|
+
): readonly ActivationSchemaIssue[] => {
|
|
320
|
+
if (Object.keys(target).length === 0) {
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const sourceOptions = jsonSchemaArrayAt(source, 'anyOf');
|
|
325
|
+
if (sourceOptions.length > 0) {
|
|
326
|
+
return sourceOptions.flatMap((option) =>
|
|
327
|
+
checkSchemaCompatibility(option, target, path)
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const targetOptions = jsonSchemaArrayAt(target, 'anyOf');
|
|
332
|
+
if (targetOptions.length > 0) {
|
|
333
|
+
const optionIssues = targetOptions.map((option) =>
|
|
334
|
+
checkSchemaCompatibility(source, option, path)
|
|
335
|
+
);
|
|
336
|
+
return optionIssues.some((issues) => issues.length === 0)
|
|
337
|
+
? []
|
|
338
|
+
: (optionIssues[0] ?? []);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const issues = [
|
|
342
|
+
...checkEnumCompatibility(source, target, path),
|
|
343
|
+
...checkTypeCompatibility(source, target, path),
|
|
344
|
+
];
|
|
345
|
+
if (issues.length > 0) {
|
|
346
|
+
return issues;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const objectIssues = (() => {
|
|
350
|
+
if (target['type'] !== 'object') {
|
|
351
|
+
return [];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const sourceRequired = new Set(stringArrayAt(source, 'required'));
|
|
355
|
+
const sourceProperties = new Map(jsonObjectEntriesAt(source, 'properties'));
|
|
356
|
+
const targetProperties = new Map(jsonObjectEntriesAt(target, 'properties'));
|
|
357
|
+
const nestedIssues: ActivationSchemaIssue[] = [];
|
|
358
|
+
|
|
359
|
+
for (const requiredKey of stringArrayAt(target, 'required')) {
|
|
360
|
+
if (
|
|
361
|
+
!sourceGuaranteesObjectProperty(
|
|
362
|
+
requiredKey,
|
|
363
|
+
sourceRequired,
|
|
364
|
+
sourceProperties
|
|
365
|
+
)
|
|
366
|
+
) {
|
|
367
|
+
nestedIssues.push(
|
|
368
|
+
compatibilityIssue(
|
|
369
|
+
'required',
|
|
370
|
+
[...path, requiredKey],
|
|
371
|
+
`Source schema does not guarantee required input field "${requiredKey}"`
|
|
372
|
+
)
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
for (const [key, targetProperty] of targetProperties) {
|
|
378
|
+
const sourceProperty = sourceProperties.get(key);
|
|
379
|
+
if (!sourceProperty) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
nestedIssues.push(
|
|
383
|
+
...checkSchemaCompatibility(sourceProperty, targetProperty, [
|
|
384
|
+
...path,
|
|
385
|
+
key,
|
|
386
|
+
])
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return nestedIssues;
|
|
391
|
+
})();
|
|
392
|
+
if (objectIssues.length > 0) {
|
|
393
|
+
return objectIssues;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (target['type'] !== 'array') {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const sourceItems = jsonObjectAt(source, 'items');
|
|
401
|
+
const targetItems = jsonObjectAt(target, 'items');
|
|
402
|
+
if (!sourceItems || !targetItems) {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return checkSchemaCompatibility(sourceItems, targetItems, [...path, '*']);
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
export const getActivationSourceInputCompatibilityIssues = (
|
|
410
|
+
inputSchema: z.ZodType<unknown>,
|
|
411
|
+
source: ActivationSource,
|
|
412
|
+
signals: ReadonlyMap<string, AnySignal>
|
|
413
|
+
): readonly ActivationSchemaIssue[] | undefined => {
|
|
414
|
+
const contract = getActivationPayloadContract(source, signals);
|
|
415
|
+
if (!contract) {
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (contract.type === 'value') {
|
|
420
|
+
const parsed = inputSchema.safeParse(contract.value);
|
|
421
|
+
return parsed.success
|
|
422
|
+
? []
|
|
423
|
+
: zodIssuesToCompatibilityIssues(parsed.error.issues);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return checkSchemaCompatibility(
|
|
427
|
+
jsonSchemaFor(contract.schema),
|
|
428
|
+
jsonSchemaFor(inputSchema)
|
|
429
|
+
);
|
|
430
|
+
};
|