@hypequery/datasets 0.14.0 → 0.16.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/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/portable-execution-errors.d.ts +49 -0
- package/dist/portable-execution-errors.d.ts.map +1 -0
- package/dist/portable-execution-errors.js +52 -0
- package/dist/portable-executor.d.ts +61 -0
- package/dist/portable-executor.d.ts.map +1 -0
- package/dist/portable-executor.js +90 -0
- package/dist/protocol-adapter.d.ts.map +1 -1
- package/dist/protocol-adapter.js +5 -97
- package/dist/protocol-rehydrate.d.ts +10 -0
- package/dist/protocol-rehydrate.d.ts.map +1 -1
- package/dist/protocol-rehydrate.js +62 -16
- package/dist/protocol-schema-adapter.d.ts +9 -0
- package/dist/protocol-schema-adapter.d.ts.map +1 -0
- package/dist/protocol-schema-adapter.js +249 -0
- package/dist/relationships.d.ts +25 -4
- package/dist/relationships.d.ts.map +1 -1
- package/dist/relationships.js +25 -4
- package/dist/semantic-query-schema.d.ts +31 -0
- package/dist/semantic-query-schema.d.ts.map +1 -1
- package/dist/semantic-query-schema.js +29 -1
- package/dist/utils/portable-execution-deadline.d.ts +4 -0
- package/dist/utils/portable-execution-deadline.d.ts.map +1 -0
- package/dist/utils/portable-execution-deadline.js +27 -0
- package/dist/utils/portable-result-budget.d.ts +12 -0
- package/dist/utils/portable-result-budget.d.ts.map +1 -0
- package/dist/utils/portable-result-budget.js +21 -0
- package/dist/utils/portable-semantic-query.d.ts +5 -0
- package/dist/utils/portable-semantic-query.d.ts.map +1 -0
- package/dist/utils/portable-semantic-query.js +36 -0
- package/dist/utils/protocol-metric-expressions.d.ts +17 -0
- package/dist/utils/protocol-metric-expressions.d.ts.map +1 -0
- package/dist/utils/protocol-metric-expressions.js +118 -0
- package/dist/utils/protocol-rehydrate-derivation.d.ts +27 -0
- package/dist/utils/protocol-rehydrate-derivation.d.ts.map +1 -0
- package/dist/utils/protocol-rehydrate-derivation.js +104 -0
- package/package.json +3 -3
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { validateCanonicalValue, validateProtocolSchema, } from '@hypequery/protocol';
|
|
2
|
+
export class ProtocolSchemaAdapterError extends TypeError {
|
|
3
|
+
path;
|
|
4
|
+
constructor(message, path = '$') {
|
|
5
|
+
super(`${message} at ${path}`);
|
|
6
|
+
this.name = 'ProtocolSchemaAdapterError';
|
|
7
|
+
this.path = path;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function canonicalValue(input, path) {
|
|
11
|
+
try {
|
|
12
|
+
if (Array.isArray(input)) {
|
|
13
|
+
return validateCanonicalValue({
|
|
14
|
+
$hypequery: {
|
|
15
|
+
type: 'array',
|
|
16
|
+
version: 1,
|
|
17
|
+
values: input.map((item, index) => canonicalValue(item, `${path}[${index}]`)),
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (typeof input === 'object' && input !== null) {
|
|
22
|
+
if ('$hypequery' in input)
|
|
23
|
+
return validateCanonicalValue(input);
|
|
24
|
+
const prototype = Object.getPrototypeOf(input);
|
|
25
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
26
|
+
throw new ProtocolSchemaAdapterError('Default is not portable plain data', path);
|
|
27
|
+
}
|
|
28
|
+
return validateCanonicalValue({
|
|
29
|
+
$hypequery: {
|
|
30
|
+
type: 'map',
|
|
31
|
+
version: 1,
|
|
32
|
+
entries: Object.entries(input)
|
|
33
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
34
|
+
.map(([key, value]) => [key, canonicalValue(value, `${path}.${key}`)]),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return validateCanonicalValue(input);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error instanceof ProtocolSchemaAdapterError)
|
|
42
|
+
throw error;
|
|
43
|
+
throw new ProtocolSchemaAdapterError('Default is not a canonical protocol value', path);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function typeName(schema) {
|
|
47
|
+
return String(schema?._def?.typeName ?? 'Unknown');
|
|
48
|
+
}
|
|
49
|
+
function description(schema) {
|
|
50
|
+
return typeof schema.description === 'string'
|
|
51
|
+
? schema.description
|
|
52
|
+
: undefined;
|
|
53
|
+
}
|
|
54
|
+
function annotate(schema, result) {
|
|
55
|
+
const value = description(schema);
|
|
56
|
+
return value === undefined ? result : { ...result, description: value };
|
|
57
|
+
}
|
|
58
|
+
function convertString(schema, path) {
|
|
59
|
+
const result = { kind: 'string' };
|
|
60
|
+
for (const check of schema._def.checks) {
|
|
61
|
+
switch (check.kind) {
|
|
62
|
+
case 'min':
|
|
63
|
+
result.minLength = check.value;
|
|
64
|
+
break;
|
|
65
|
+
case 'max':
|
|
66
|
+
result.maxLength = check.value;
|
|
67
|
+
break;
|
|
68
|
+
case 'length':
|
|
69
|
+
result.minLength = check.value;
|
|
70
|
+
result.maxLength = check.value;
|
|
71
|
+
break;
|
|
72
|
+
default:
|
|
73
|
+
throw new ProtocolSchemaAdapterError(`Unsupported Zod string check "${String(check.kind)}"`, path);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return annotate(schema, result);
|
|
77
|
+
}
|
|
78
|
+
function convertNumber(schema, path) {
|
|
79
|
+
const result = { kind: 'number' };
|
|
80
|
+
for (const check of schema._def.checks) {
|
|
81
|
+
switch (check.kind) {
|
|
82
|
+
case 'int':
|
|
83
|
+
result.kind = 'integer';
|
|
84
|
+
break;
|
|
85
|
+
case 'min':
|
|
86
|
+
result[check.inclusive === false ? 'exclusiveMinimum' : 'minimum'] = check.value;
|
|
87
|
+
break;
|
|
88
|
+
case 'max':
|
|
89
|
+
result[check.inclusive === false ? 'exclusiveMaximum' : 'maximum'] = check.value;
|
|
90
|
+
break;
|
|
91
|
+
case 'finite':
|
|
92
|
+
break;
|
|
93
|
+
default:
|
|
94
|
+
throw new ProtocolSchemaAdapterError(`Unsupported Zod number check "${String(check.kind)}"`, path);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return annotate(schema, result);
|
|
98
|
+
}
|
|
99
|
+
function isOptionalProperty(schema) {
|
|
100
|
+
const name = typeName(schema);
|
|
101
|
+
return name === 'ZodOptional' || name === 'ZodDefault';
|
|
102
|
+
}
|
|
103
|
+
function nativeEnumValues(input) {
|
|
104
|
+
const values = Object.keys(input)
|
|
105
|
+
.filter(key => typeof input[String(input[key])] !== 'number')
|
|
106
|
+
.map(key => input[key]);
|
|
107
|
+
return [...new Set(values)];
|
|
108
|
+
}
|
|
109
|
+
function isUnconstrainedRecordKey(schema) {
|
|
110
|
+
if (schema === undefined || typeName(schema) !== 'ZodString')
|
|
111
|
+
return false;
|
|
112
|
+
const definition = schema._def;
|
|
113
|
+
return Array.isArray(definition.checks)
|
|
114
|
+
&& definition.checks.length === 0
|
|
115
|
+
&& definition.coerce !== true;
|
|
116
|
+
}
|
|
117
|
+
function convert(schema, path) {
|
|
118
|
+
const definition = schema._def;
|
|
119
|
+
switch (typeName(schema)) {
|
|
120
|
+
case 'ZodAny':
|
|
121
|
+
case 'ZodUnknown':
|
|
122
|
+
return annotate(schema, { kind: 'any' });
|
|
123
|
+
case 'ZodNever':
|
|
124
|
+
case 'ZodVoid':
|
|
125
|
+
case 'ZodUndefined':
|
|
126
|
+
return annotate(schema, { kind: 'void' });
|
|
127
|
+
case 'ZodNull':
|
|
128
|
+
return annotate(schema, { kind: 'null' });
|
|
129
|
+
case 'ZodBoolean':
|
|
130
|
+
return annotate(schema, { kind: 'boolean' });
|
|
131
|
+
case 'ZodString':
|
|
132
|
+
return convertString(schema, path);
|
|
133
|
+
case 'ZodNumber':
|
|
134
|
+
return convertNumber(schema, path);
|
|
135
|
+
case 'ZodLiteral':
|
|
136
|
+
return annotate(schema, {
|
|
137
|
+
kind: 'literal',
|
|
138
|
+
value: canonicalValue(definition.value, `${path}.value`),
|
|
139
|
+
});
|
|
140
|
+
case 'ZodEnum':
|
|
141
|
+
return annotate(schema, {
|
|
142
|
+
kind: 'enum',
|
|
143
|
+
values: [...definition.values],
|
|
144
|
+
});
|
|
145
|
+
case 'ZodNativeEnum': {
|
|
146
|
+
const values = nativeEnumValues(definition.values);
|
|
147
|
+
return annotate(schema, { kind: 'enum', values });
|
|
148
|
+
}
|
|
149
|
+
case 'ZodArray': {
|
|
150
|
+
const result = {
|
|
151
|
+
kind: 'array',
|
|
152
|
+
items: convert(definition.type, `${path}.items`),
|
|
153
|
+
};
|
|
154
|
+
if (definition.minLength?.value !== undefined)
|
|
155
|
+
result.minItems = definition.minLength.value;
|
|
156
|
+
if (definition.maxLength?.value !== undefined)
|
|
157
|
+
result.maxItems = definition.maxLength.value;
|
|
158
|
+
if (definition.exactLength?.value !== undefined) {
|
|
159
|
+
result.minItems = definition.exactLength.value;
|
|
160
|
+
result.maxItems = definition.exactLength.value;
|
|
161
|
+
}
|
|
162
|
+
return annotate(schema, result);
|
|
163
|
+
}
|
|
164
|
+
case 'ZodObject': {
|
|
165
|
+
if (typeName(definition.catchall) !== 'ZodNever') {
|
|
166
|
+
throw new ProtocolSchemaAdapterError('Unsupported Zod object catchall', path);
|
|
167
|
+
}
|
|
168
|
+
const shape = definition.shape();
|
|
169
|
+
const properties = Object.fromEntries(Object.entries(shape).map(([name, property]) => [
|
|
170
|
+
name,
|
|
171
|
+
convert(property, `${path}.properties.${name}`),
|
|
172
|
+
]));
|
|
173
|
+
const required = Object.entries(shape)
|
|
174
|
+
.filter(([, property]) => !isOptionalProperty(property))
|
|
175
|
+
.map(([name]) => name);
|
|
176
|
+
const unknownProperties = definition.unknownKeys === 'passthrough'
|
|
177
|
+
? 'preserve'
|
|
178
|
+
: definition.unknownKeys === 'strict'
|
|
179
|
+
? 'reject'
|
|
180
|
+
: 'strip';
|
|
181
|
+
return annotate(schema, {
|
|
182
|
+
kind: 'object',
|
|
183
|
+
properties,
|
|
184
|
+
required,
|
|
185
|
+
unknownProperties,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
case 'ZodRecord':
|
|
189
|
+
if (!isUnconstrainedRecordKey(definition.keyType)) {
|
|
190
|
+
throw new ProtocolSchemaAdapterError('Unsupported constrained Zod record key', path);
|
|
191
|
+
}
|
|
192
|
+
return annotate(schema, {
|
|
193
|
+
kind: 'record',
|
|
194
|
+
values: convert(definition.valueType, `${path}.values`),
|
|
195
|
+
});
|
|
196
|
+
case 'ZodUnion':
|
|
197
|
+
return annotate(schema, {
|
|
198
|
+
kind: 'union',
|
|
199
|
+
variants: definition.options.map((option, index) => convert(option, `${path}.variants[${index}]`)),
|
|
200
|
+
});
|
|
201
|
+
case 'ZodDiscriminatedUnion':
|
|
202
|
+
// RFC 0004 has no separate discriminator index. Converting every option
|
|
203
|
+
// drops only Zod's lookup metadata; each object's literal discriminator
|
|
204
|
+
// property remains part of its protocol variant.
|
|
205
|
+
return annotate(schema, {
|
|
206
|
+
kind: 'union',
|
|
207
|
+
variants: [...definition.options.values()].map((option, index) => convert(option, `${path}.variants[${index}]`)),
|
|
208
|
+
});
|
|
209
|
+
case 'ZodNullable':
|
|
210
|
+
return annotate(schema, {
|
|
211
|
+
kind: 'union',
|
|
212
|
+
variants: [convert(definition.innerType, `${path}.variants[0]`), { kind: 'null' }],
|
|
213
|
+
});
|
|
214
|
+
case 'ZodOptional':
|
|
215
|
+
return annotate(schema, convert(definition.innerType, path));
|
|
216
|
+
case 'ZodDefault': {
|
|
217
|
+
const inner = convert(definition.innerType, path);
|
|
218
|
+
if (inner.kind === 'void') {
|
|
219
|
+
throw new ProtocolSchemaAdapterError('Void schemas cannot carry defaults', path);
|
|
220
|
+
}
|
|
221
|
+
return annotate(schema, {
|
|
222
|
+
...inner,
|
|
223
|
+
default: canonicalValue(definition.defaultValue(), `${path}.default`),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
case 'ZodBranded':
|
|
227
|
+
return annotate(schema, convert(definition.type, path));
|
|
228
|
+
case 'ZodReadonly':
|
|
229
|
+
return annotate(schema, convert(definition.innerType, path));
|
|
230
|
+
case 'ZodEffects': {
|
|
231
|
+
const effectType = String(definition.effect?.type ?? 'unknown');
|
|
232
|
+
if (effectType !== 'refinement') {
|
|
233
|
+
throw new ProtocolSchemaAdapterError(`Unsupported Zod effect "${effectType}"`, path);
|
|
234
|
+
}
|
|
235
|
+
// A refinement preserves the wrapped schema's input and output shape.
|
|
236
|
+
// The rule itself remains enforced by Zod, but cannot be advertised by
|
|
237
|
+
// ProtocolSchema (for example, a cross-field selection requirement).
|
|
238
|
+
return annotate(schema, convert(definition.schema, path));
|
|
239
|
+
}
|
|
240
|
+
default:
|
|
241
|
+
throw new ProtocolSchemaAdapterError(`Unsupported Zod type "${typeName(schema)}"`, path);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** Converts the portable subset of a Zod v3 schema into RFC 0004. */
|
|
245
|
+
export function zodToProtocolSchema(schema, path = '$') {
|
|
246
|
+
if (schema === undefined)
|
|
247
|
+
return validateProtocolSchema({ kind: 'any' });
|
|
248
|
+
return validateProtocolSchema(convert(schema, path));
|
|
249
|
+
}
|
package/dist/relationships.d.ts
CHANGED
|
@@ -1,19 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Relationship helpers for dataset definitions.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* To-one relationships (`belongsTo`, `hasOne`) are queryable one hop deep as
|
|
5
|
+
* `<relationship>.<dimension>` and execute as LEFT JOINs. `hasMany` is metadata
|
|
6
|
+
* only: joining it would fan out and corrupt aggregates, so it is refused at
|
|
7
|
+
* query time.
|
|
8
|
+
*
|
|
9
|
+
* Only the target's *dimensions* are reachable. A measure on the target dataset
|
|
10
|
+
* is not addressable through a relationship, so there are no cross-dataset
|
|
11
|
+
* metrics.
|
|
12
|
+
*
|
|
13
|
+
* Note that the `kind` passed here is a declaration, not something checked
|
|
14
|
+
* against the data — there is no uniqueness concept in the model, so nothing
|
|
15
|
+
* verifies that a `belongsTo` target column really is unique. What a
|
|
16
|
+
* mis-declaration costs depends on the builder. One that implements the
|
|
17
|
+
* optional `leftAnyJoin` (ClickHouse `LEFT ANY JOIN`) takes at most one target
|
|
18
|
+
* row per base row, so the aggregate cannot inflate — it just silently picks an
|
|
19
|
+
* arbitrary one of the matches. A builder without it falls back to `leftJoin`,
|
|
20
|
+
* where duplicate target keys fan out and do inflate the aggregate. Either way
|
|
21
|
+
* the declaration has to be right.
|
|
7
22
|
*
|
|
8
23
|
* @example
|
|
9
24
|
* ```ts
|
|
10
25
|
* const Orders = dataset("orders", {
|
|
11
26
|
* source: "orders",
|
|
12
|
-
*
|
|
27
|
+
* dimensions: {
|
|
28
|
+
* id: dimension.string(),
|
|
29
|
+
* customerId: dimension.string({ column: "customer_id" }),
|
|
30
|
+
* },
|
|
13
31
|
* relationships: {
|
|
14
32
|
* customer: belongsTo(() => Customers, { from: "customerId", to: "id" }),
|
|
15
33
|
* },
|
|
16
34
|
* });
|
|
35
|
+
*
|
|
36
|
+
* // Groups by a dimension on Customers, joining through the relationship.
|
|
37
|
+
* await client.execute(Orders, { dimensions: ["customer.country"] });
|
|
17
38
|
* ```
|
|
18
39
|
*/
|
|
19
40
|
import type { RelationshipDefinition } from './types.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relationships.d.ts","sourceRoot":"","sources":["../src/relationships.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"relationships.d.ts","sourceRoot":"","sources":["../src/relationships.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAoB,MAAM,YAAY,CAAC;AAmB3E,mDAAmD;AACnD,wBAAgB,SAAS,CAAC,OAAO,SAAS;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAC3E,MAAM,EAAE,MAAM,OAAO,EACrB,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACjC,sBAAsB,CAAC,OAAO,EAAE,WAAW,CAAC,CAE9C;AAED,qDAAqD;AACrD,wBAAgB,OAAO,CAAC,OAAO,SAAS;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACzE,MAAM,EAAE,MAAM,OAAO,EACrB,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACjC,sBAAsB,CAAC,OAAO,EAAE,SAAS,CAAC,CAE5C;AAED,oDAAoD;AACpD,wBAAgB,MAAM,CAAC,OAAO,SAAS;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACxE,MAAM,EAAE,MAAM,OAAO,EACrB,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACjC,sBAAsB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAE3C"}
|
package/dist/relationships.js
CHANGED
|
@@ -1,19 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Relationship helpers for dataset definitions.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* To-one relationships (`belongsTo`, `hasOne`) are queryable one hop deep as
|
|
5
|
+
* `<relationship>.<dimension>` and execute as LEFT JOINs. `hasMany` is metadata
|
|
6
|
+
* only: joining it would fan out and corrupt aggregates, so it is refused at
|
|
7
|
+
* query time.
|
|
8
|
+
*
|
|
9
|
+
* Only the target's *dimensions* are reachable. A measure on the target dataset
|
|
10
|
+
* is not addressable through a relationship, so there are no cross-dataset
|
|
11
|
+
* metrics.
|
|
12
|
+
*
|
|
13
|
+
* Note that the `kind` passed here is a declaration, not something checked
|
|
14
|
+
* against the data — there is no uniqueness concept in the model, so nothing
|
|
15
|
+
* verifies that a `belongsTo` target column really is unique. What a
|
|
16
|
+
* mis-declaration costs depends on the builder. One that implements the
|
|
17
|
+
* optional `leftAnyJoin` (ClickHouse `LEFT ANY JOIN`) takes at most one target
|
|
18
|
+
* row per base row, so the aggregate cannot inflate — it just silently picks an
|
|
19
|
+
* arbitrary one of the matches. A builder without it falls back to `leftJoin`,
|
|
20
|
+
* where duplicate target keys fan out and do inflate the aggregate. Either way
|
|
21
|
+
* the declaration has to be right.
|
|
7
22
|
*
|
|
8
23
|
* @example
|
|
9
24
|
* ```ts
|
|
10
25
|
* const Orders = dataset("orders", {
|
|
11
26
|
* source: "orders",
|
|
12
|
-
*
|
|
27
|
+
* dimensions: {
|
|
28
|
+
* id: dimension.string(),
|
|
29
|
+
* customerId: dimension.string({ column: "customer_id" }),
|
|
30
|
+
* },
|
|
13
31
|
* relationships: {
|
|
14
32
|
* customer: belongsTo(() => Customers, { from: "customerId", to: "id" }),
|
|
15
33
|
* },
|
|
16
34
|
* });
|
|
35
|
+
*
|
|
36
|
+
* // Groups by a dimension on Customers, joining through the relationship.
|
|
37
|
+
* await client.execute(Orders, { dimensions: ["customer.country"] });
|
|
17
38
|
* ```
|
|
18
39
|
*/
|
|
19
40
|
function createRelationship(kind, target, join) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ZodTypeAny } from 'zod';
|
|
2
2
|
import { type DatasetCatalog, type DatasetCatalogSource, type MetricCatalogEntry } from './catalog.js';
|
|
3
3
|
import type { JsonSchema } from './tools.js';
|
|
4
|
+
import type { ProtocolSchema } from '@hypequery/protocol';
|
|
4
5
|
export interface SemanticQuerySchemaLimits {
|
|
5
6
|
defaultResultSize?: number;
|
|
6
7
|
maxResultSize?: number;
|
|
@@ -19,6 +20,18 @@ export interface SemanticQuerySchemaOptions extends SemanticQuerySchemaLimits {
|
|
|
19
20
|
requireSelection?: boolean;
|
|
20
21
|
/** Validate result limits in the schema. Disable when a consumer clamps them. */
|
|
21
22
|
enforceResultLimit?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Datasets that may be queried directly. Defaults to all of them.
|
|
25
|
+
*
|
|
26
|
+
* A dataset outside this list still contributes its metrics and can still be
|
|
27
|
+
* joined to, but is not offered as a `query_dataset` target. That split is not
|
|
28
|
+
* hypothetical: a deployment contract authorizes a dataset and each of its
|
|
29
|
+
* metrics through separate endpoint policies, so a caller can be entitled to
|
|
30
|
+
* a metric on a dataset it may not query directly. Compiling one schema for
|
|
31
|
+
* both would either advertise a target the caller cannot use or hide a metric
|
|
32
|
+
* it can.
|
|
33
|
+
*/
|
|
34
|
+
queryableDatasets?: readonly string[];
|
|
22
35
|
}
|
|
23
36
|
/** Metric-specific query capabilities used when they are not embedded in the Dataset source. */
|
|
24
37
|
export type SemanticMetricQueryContract = Pick<MetricCatalogEntry, 'dimensions' | 'filters' | 'grains' | 'grain'>;
|
|
@@ -45,4 +58,22 @@ export declare function toSemanticJsonSchema(schema: ZodTypeAny, options?: {
|
|
|
45
58
|
requireSelection?: boolean;
|
|
46
59
|
}): JsonSchema;
|
|
47
60
|
export declare function buildCanonicalSemanticQuerySchemas(datasets: Record<string, SemanticQuerySchemaSource>, options?: SemanticQuerySchemaOptions): CanonicalSemanticQuerySchemas;
|
|
61
|
+
/**
|
|
62
|
+
* The same input schema, as a `ProtocolSchema`.
|
|
63
|
+
*
|
|
64
|
+
* A hosted registry advertises one endpoint per dataset and per metric, and it
|
|
65
|
+
* advertises them in the protocol's own schema format rather than Zod or JSON
|
|
66
|
+
* Schema. Without this it has to rebuild the shape by hand — which is a second
|
|
67
|
+
* generator of the knowledge `CORE-03` exists to keep in one place, and it
|
|
68
|
+
* drifts silently: an advertised field the validator rejects, or a ceiling the
|
|
69
|
+
* data plane does not apply.
|
|
70
|
+
*
|
|
71
|
+
* Derived from `buildDatasetInputSchema`, so there is one source for which
|
|
72
|
+
* dimensions are groupable, which measures exist, what the filter operators
|
|
73
|
+
* are, what the orderable fields are, which grains are supported, and what the
|
|
74
|
+
* row ceiling is.
|
|
75
|
+
*/
|
|
76
|
+
export declare function buildDatasetInputProtocolSchema(dataset: SemanticQuerySchemaSource, options?: SemanticQuerySchemaOptions, path?: string): ProtocolSchema;
|
|
77
|
+
/** The metric form of {@link buildDatasetInputProtocolSchema}. */
|
|
78
|
+
export declare function buildMetricInputProtocolSchema(dataset: SemanticQuerySchemaSource, metricName: string, options?: SemanticQuerySchemaOptions, metricContract?: SemanticMetricQueryContract, path?: string): ProtocolSchema;
|
|
48
79
|
//# sourceMappingURL=semantic-query-schema.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semantic-query-schema.d.ts","sourceRoot":"","sources":["../src/semantic-query-schema.ts"],"names":[],"mappings":"AAEA,OAAO,EAAK,KAAK,UAAU,EAAE,MAAM,KAAK,CAAC;AAEzC,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACxB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"semantic-query-schema.d.ts","sourceRoot":"","sources":["../src/semantic-query-schema.ts"],"names":[],"mappings":"AAEA,OAAO,EAAK,KAAK,UAAU,EAAE,MAAM,KAAK,CAAC;AAEzC,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACxB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAI1D,MAAM,WAAW,yBAAyB;IACxC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,0BAA2B,SAAQ,yBAAyB;IAC3E,iFAAiF;IACjF,UAAU,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC;IAC5B,mDAAmD;IACnD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,qEAAqE;IACrE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,iFAAiF;IACjF,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED,gGAAgG;AAChG,MAAM,MAAM,2BAA2B,GAAG,IAAI,CAC5C,kBAAkB,EAClB,YAAY,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAC9C,CAAC;AAEF,eAAO,MAAM,oCAAoC;;;;;;;EAO/C,CAAC;AAEH,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,YAAY,EAAE,UAAU,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC;IACjC,QAAQ,CAAC,sBAAsB,EAAE,UAAU,CAAC;IAC5C,QAAQ,CAAC,qBAAqB,EAAE,UAAU,CAAC;IAC3C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,oFAAoF;AACpF,MAAM,MAAM,yBAAyB,GAAG,oBAAoB,GAAG,cAAc,CAAC;AA6I9E,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,yBAAyB,EAClC,OAAO,GAAE,0BAA+B,GACvC,UAAU,CAOZ;AAED,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,yBAAyB,EAClC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,0BAA+B,EACxC,cAAc,CAAC,EAAE,2BAA2B,GAC3C,UAAU,CAaZ;AAsBD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,UAAU,EAClB,OAAO,GAAE;IAAE,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAAO,GAC3C,UAAU,CAMZ;AAyBD,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,EACnD,OAAO,GAAE,0BAA+B,GACvC,6BAA6B,CAgD/B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,yBAAyB,EAClC,OAAO,GAAE,0BAA+B,EACxC,IAAI,SAAkB,GACrB,cAAc,CAEhB;AAED,kEAAkE;AAClE,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,yBAAyB,EAClC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,0BAA+B,EACxC,cAAc,CAAC,EAAE,2BAA2B,EAC5C,IAAI,SAAiB,GACpB,cAAc,CAKhB"}
|
|
@@ -4,6 +4,7 @@ import { z } from 'zod';
|
|
|
4
4
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
5
5
|
import { getDatasetCatalog, getGroupableRelationshipFields, getQueryableRelationshipFields, } from './catalog.js';
|
|
6
6
|
import { SEMANTIC_FILTER_OPERATORS } from './constants.js';
|
|
7
|
+
import { zodToProtocolSchema } from './protocol-schema-adapter.js';
|
|
7
8
|
import { compareStrings, stableStringify, uniqueSorted } from './utils/canonical-json.js';
|
|
8
9
|
export const DEFAULT_SEMANTIC_QUERY_SCHEMA_LIMITS = Object.freeze({
|
|
9
10
|
maxResultSize: 10_000,
|
|
@@ -187,9 +188,14 @@ function addSelectionRequirement(schema) {
|
|
|
187
188
|
export function buildCanonicalSemanticQuerySchemas(datasets, options = {}) {
|
|
188
189
|
const datasetSchemas = [];
|
|
189
190
|
const metricSchemas = [];
|
|
191
|
+
const queryable = options.queryableDatasets === undefined
|
|
192
|
+
? undefined
|
|
193
|
+
: new Set(options.queryableDatasets);
|
|
190
194
|
for (const [datasetName, dataset] of Object.entries(datasets)
|
|
191
195
|
.sort(([left], [right]) => compareStrings(left, right))) {
|
|
192
|
-
|
|
196
|
+
if (queryable === undefined || queryable.has(datasetName)) {
|
|
197
|
+
datasetSchemas.push(withSelectors(buildDatasetInputSchema(dataset, { ...options, requireSelection: false }), { dataset: z.literal(datasetName) }, options.requireSelection !== false));
|
|
198
|
+
}
|
|
193
199
|
const catalog = resolveCatalog(dataset);
|
|
194
200
|
for (const metricName of Object.keys(catalog.metrics).sort(compareStrings)) {
|
|
195
201
|
metricSchemas.push(withSelectors(buildMetricInputSchema(dataset, metricName, options, catalog.metrics[metricName]), { dataset: z.literal(datasetName), metric: z.literal(metricName) }, false));
|
|
@@ -216,3 +222,25 @@ export function buildCanonicalSemanticQuerySchemas(datasets, options = {}) {
|
|
|
216
222
|
manifestHash,
|
|
217
223
|
});
|
|
218
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* The same input schema, as a `ProtocolSchema`.
|
|
227
|
+
*
|
|
228
|
+
* A hosted registry advertises one endpoint per dataset and per metric, and it
|
|
229
|
+
* advertises them in the protocol's own schema format rather than Zod or JSON
|
|
230
|
+
* Schema. Without this it has to rebuild the shape by hand — which is a second
|
|
231
|
+
* generator of the knowledge `CORE-03` exists to keep in one place, and it
|
|
232
|
+
* drifts silently: an advertised field the validator rejects, or a ceiling the
|
|
233
|
+
* data plane does not apply.
|
|
234
|
+
*
|
|
235
|
+
* Derived from `buildDatasetInputSchema`, so there is one source for which
|
|
236
|
+
* dimensions are groupable, which measures exist, what the filter operators
|
|
237
|
+
* are, what the orderable fields are, which grains are supported, and what the
|
|
238
|
+
* row ceiling is.
|
|
239
|
+
*/
|
|
240
|
+
export function buildDatasetInputProtocolSchema(dataset, options = {}, path = 'dataset.input') {
|
|
241
|
+
return zodToProtocolSchema(buildDatasetInputSchema(dataset, options), path);
|
|
242
|
+
}
|
|
243
|
+
/** The metric form of {@link buildDatasetInputProtocolSchema}. */
|
|
244
|
+
export function buildMetricInputProtocolSchema(dataset, metricName, options = {}, metricContract, path = 'metric.input') {
|
|
245
|
+
return zodToProtocolSchema(buildMetricInputSchema(dataset, metricName, options, metricContract), path);
|
|
246
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PortableSemanticBudget } from '../portable-executor.js';
|
|
2
|
+
/** Abort the underlying request and independently settle the invocation on expiry. */
|
|
3
|
+
export declare function withDeadline<T>(budget: PortableSemanticBudget, signal: AbortSignal | undefined, execute: (signal: AbortSignal) => Promise<T>): Promise<T>;
|
|
4
|
+
//# sourceMappingURL=portable-execution-deadline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portable-execution-deadline.d.ts","sourceRoot":"","sources":["../../src/utils/portable-execution-deadline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAGtE,sFAAsF;AACtF,wBAAsB,YAAY,CAAC,CAAC,EAClC,MAAM,EAAE,sBAAsB,EAC9B,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,OAAO,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3C,OAAO,CAAC,CAAC,CAAC,CAuBZ"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { PortableExecutionBudgetError } from '../portable-execution-errors.js';
|
|
2
|
+
/** Abort the underlying request and independently settle the invocation on expiry. */
|
|
3
|
+
export async function withDeadline(budget, signal, execute) {
|
|
4
|
+
if (signal?.aborted)
|
|
5
|
+
throw signal.reason ?? new Error('The invocation was cancelled.');
|
|
6
|
+
const controller = new AbortController();
|
|
7
|
+
const abort = () => controller.abort(signal?.reason);
|
|
8
|
+
let rejectAbort = () => { };
|
|
9
|
+
const cancelled = new Promise((_resolve, reject) => {
|
|
10
|
+
rejectAbort = () => reject(controller.signal.reason);
|
|
11
|
+
controller.signal.addEventListener('abort', rejectAbort, { once: true });
|
|
12
|
+
});
|
|
13
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
14
|
+
const timer = budget.deadlineMs === undefined ? undefined : setTimeout(() => {
|
|
15
|
+
controller.abort(new PortableExecutionBudgetError(`The invocation exceeded its ${String(budget.deadlineMs)}ms deadline.`));
|
|
16
|
+
}, budget.deadlineMs);
|
|
17
|
+
try {
|
|
18
|
+
// Promise.race also observes a late driver rejection after cancellation.
|
|
19
|
+
return await Promise.race([cancelled, execute(controller.signal)]);
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
if (timer !== undefined)
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
signal?.removeEventListener('abort', abort);
|
|
25
|
+
controller.signal.removeEventListener('abort', rejectAbort);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ProtocolSemanticInvocationResult } from '@hypequery/protocol';
|
|
2
|
+
import type { PortableSemanticBudget } from '../portable-executor.js';
|
|
3
|
+
/**
|
|
4
|
+
* Bounds a result by row count and serialized size before it leaves the
|
|
5
|
+
* deployment, so an oversized answer fails rather than being streamed on.
|
|
6
|
+
*
|
|
7
|
+
* The byte budget is measured against the whole record a caller receives, not
|
|
8
|
+
* only its rows, so this agrees with the data plane's own check rather than
|
|
9
|
+
* undercounting by the envelope.
|
|
10
|
+
*/
|
|
11
|
+
export declare function bounded(result: ProtocolSemanticInvocationResult, budget: PortableSemanticBudget): ProtocolSemanticInvocationResult;
|
|
12
|
+
//# sourceMappingURL=portable-result-budget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portable-result-budget.d.ts","sourceRoot":"","sources":["../../src/utils/portable-result-budget.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAGtE;;;;;;;GAOG;AACH,wBAAgB,OAAO,CACrB,MAAM,EAAE,gCAAgC,EACxC,MAAM,EAAE,sBAAsB,GAC7B,gCAAgC,CAelC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PortableExecutionBudgetError } from '../portable-execution-errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Bounds a result by row count and serialized size before it leaves the
|
|
4
|
+
* deployment, so an oversized answer fails rather than being streamed on.
|
|
5
|
+
*
|
|
6
|
+
* The byte budget is measured against the whole record a caller receives, not
|
|
7
|
+
* only its rows, so this agrees with the data plane's own check rather than
|
|
8
|
+
* undercounting by the envelope.
|
|
9
|
+
*/
|
|
10
|
+
export function bounded(result, budget) {
|
|
11
|
+
if (result.data.length > budget.maxRows) {
|
|
12
|
+
throw new PortableExecutionBudgetError(`The result has ${result.data.length} rows; the effective limit is ${budget.maxRows}.`);
|
|
13
|
+
}
|
|
14
|
+
if (budget.maxResponseBytes !== undefined) {
|
|
15
|
+
const bytes = new TextEncoder().encode(JSON.stringify(result)).byteLength;
|
|
16
|
+
if (bytes > budget.maxResponseBytes) {
|
|
17
|
+
throw new PortableExecutionBudgetError(`The result is ${bytes} bytes; the effective limit is ${budget.maxResponseBytes}.`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ProtocolSemanticQuery } from '@hypequery/protocol';
|
|
2
|
+
import type { SemanticTenantRuntime } from '../types.js';
|
|
3
|
+
export declare function tenantRuntime(tenant: unknown): SemanticTenantRuntime | undefined;
|
|
4
|
+
export declare function semanticQuery(operation: ProtocolSemanticQuery, maxRows: number): Record<string, unknown>;
|
|
5
|
+
//# sourceMappingURL=portable-semantic-query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portable-semantic-query.d.ts","sourceRoot":"","sources":["../../src/utils/portable-semantic-query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EAA+B,qBAAqB,EAAa,MAAM,aAAa,CAAC;AAIjG,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,GAAG,qBAAqB,GAAG,SAAS,CAKhF;AAWD,wBAAgB,aAAa,CAAC,SAAS,EAAE,qBAAqB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAqBxG"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { PortableExecutionUnsupportedError } from '../portable-execution-errors.js';
|
|
2
|
+
import { rehydrateMeasureFilter } from './protocol-rehydrate-filters.js';
|
|
3
|
+
export function tenantRuntime(tenant) {
|
|
4
|
+
if (tenant === undefined || tenant === null)
|
|
5
|
+
return undefined;
|
|
6
|
+
if (typeof tenant === 'string')
|
|
7
|
+
return tenant;
|
|
8
|
+
if (Array.isArray(tenant))
|
|
9
|
+
return { in: tenant.map(String) };
|
|
10
|
+
return tenant;
|
|
11
|
+
}
|
|
12
|
+
function operationFilters(operation) {
|
|
13
|
+
return (operation.filters ?? []).map((expression, index) => rehydrateMeasureFilter(expression, () => new PortableExecutionUnsupportedError(`Filter ${index} is not a field/operator/value comparison, so it cannot be planned.`)));
|
|
14
|
+
}
|
|
15
|
+
export function semanticQuery(operation, maxRows) {
|
|
16
|
+
const filters = operationFilters(operation);
|
|
17
|
+
const orderBy = (operation.orderBy ?? []).map(entry => ({
|
|
18
|
+
field: String(entry.field),
|
|
19
|
+
direction: entry.direction,
|
|
20
|
+
}));
|
|
21
|
+
return {
|
|
22
|
+
...(operation.dimensions === undefined
|
|
23
|
+
? {}
|
|
24
|
+
: { dimensions: operation.dimensions.map(String) }),
|
|
25
|
+
...(operation.kind === 'dataset' && operation.measures !== undefined
|
|
26
|
+
? { measures: operation.measures.map(String) }
|
|
27
|
+
: {}),
|
|
28
|
+
...(filters.length > 0 ? { filters } : {}),
|
|
29
|
+
...(orderBy.length > 0 ? { orderBy } : {}),
|
|
30
|
+
// An omitted limit becomes the budget, so a caller cannot ask for an
|
|
31
|
+
// unbounded scan by leaving it out.
|
|
32
|
+
limit: operation.limit ?? maxRows,
|
|
33
|
+
...(operation.offset === undefined ? {} : { offset: operation.offset }),
|
|
34
|
+
...(operation.by === undefined ? {} : { by: operation.by }),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ProtocolExpression, type ProtocolMetricDerivation } from '@hypequery/protocol';
|
|
2
|
+
import type { AggregationSpec, DerivedMetricSpec, MetricFilter } from '../types.js';
|
|
3
|
+
export declare function filterExpression(filter: MetricFilter): ProtocolExpression;
|
|
4
|
+
export declare function metricExpression(spec: AggregationSpec | DerivedMetricSpec): ProtocolExpression;
|
|
5
|
+
/**
|
|
6
|
+
* The formula in the shape it was authored in, beside the inlined form.
|
|
7
|
+
*
|
|
8
|
+
* `metricExpression` above substitutes each input's aggregate where the formula
|
|
9
|
+
* named it, which states what the metric means but drops the aliases. Those
|
|
10
|
+
* aliases are the column names of the intermediate aggregate, so a catalog
|
|
11
|
+
* rebuilt without them computes the same number through different SQL.
|
|
12
|
+
*
|
|
13
|
+
* Input order follows `uses` and is not sorted: each entry becomes a column of
|
|
14
|
+
* that intermediate result in this order.
|
|
15
|
+
*/
|
|
16
|
+
export declare function metricDerivation(spec: DerivedMetricSpec): ProtocolMetricDerivation;
|
|
17
|
+
//# sourceMappingURL=protocol-metric-expressions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol-metric-expressions.d.ts","sourceRoot":"","sources":["../../src/utils/protocol-metric-expressions.ts"],"names":[],"mappings":"AAAA,OAAO,EAEgB,KAAK,kBAAkB,EAAE,KAAK,wBAAwB,EAC5E,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAsCpF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,GAAG,kBAAkB,CAgBzE;AA+CD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,iBAAiB,GAAG,kBAAkB,CAQ9F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,wBAAwB,CASlF"}
|