@query-graph/core 1.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dsl.js ADDED
@@ -0,0 +1,742 @@
1
+ 'use strict'
2
+
3
+ const GRAPH_DEFINITION_VERSION = 9
4
+ const SCALAR_TYPES = new Set([
5
+ 'boolean',
6
+ 'int32',
7
+ 'int64',
8
+ 'float64',
9
+ 'decimal',
10
+ 'string',
11
+ 'date',
12
+ 'dateTime',
13
+ 'binary',
14
+ 'json',
15
+ ])
16
+ const GRAPH_MODULE_BRAND = Symbol('GraphModule')
17
+ const SUMMARY_FIELD_BRAND = Symbol('SummaryField')
18
+
19
+ const RELATION_CONFIGURATION_KEYS = new Set(['name', 'from', 'to', 'on', 'required', 'cardinality', 'selection'])
20
+ const CONSTRAINT_CONFIGURATION_KEYS = new Set(['predicate', 'when'])
21
+ const PROJECTION_CONFIGURATION_KEYS = new Set(['path', 'expression', 'default'])
22
+ const EXISTS_CONFIGURATION_KEYS = new Set(['from'])
23
+ const ORDERING_CONFIGURATION_KEYS = new Set(['name', 'by', 'default'])
24
+
25
+ function configurationOf(factory, value, allowedKeys) {
26
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
27
+ throw new TypeError(`${factory} expects a configuration object`)
28
+ }
29
+
30
+ const unknownKeys = Reflect.ownKeys(value).filter((key) => typeof key !== 'string' || !allowedKeys.has(key))
31
+ if (unknownKeys.length > 0) {
32
+ const label = unknownKeys.length === 1 ? 'field' : 'fields'
33
+ const keys = unknownKeys.map((key) => JSON.stringify(String(key))).join(', ')
34
+ throw new TypeError(`${factory} received unknown configuration ${label} ${keys}`)
35
+ }
36
+
37
+ return value
38
+ }
39
+
40
+ function fieldType(scalarType, options = {}) {
41
+ assertScalarType(scalarType)
42
+
43
+ const definition = { scalarType }
44
+ if (options.nullable === true) {
45
+ definition.nullable = true
46
+ }
47
+ if (options.selectable === false) {
48
+ definition.selectable = false
49
+ }
50
+ return definition
51
+ }
52
+
53
+ function nullable(specification) {
54
+ return {
55
+ ...normalizeFieldSpec(specification),
56
+ nullable: true,
57
+ }
58
+ }
59
+
60
+ function hidden(specification) {
61
+ return {
62
+ ...normalizeFieldSpec(specification),
63
+ selectable: false,
64
+ }
65
+ }
66
+
67
+ function source(key, fieldSpecifications) {
68
+ const fields = Object.entries(fieldSpecifications).map(([name, specification]) => ({
69
+ name,
70
+ ...normalizeFieldSpec(specification),
71
+ }))
72
+ const sourceReference = { key, fields }
73
+
74
+ Object.defineProperty(sourceReference, 'field', {
75
+ enumerable: false,
76
+ value(name) {
77
+ return field(sourceReference, name)
78
+ },
79
+ })
80
+
81
+ return sourceReference
82
+ }
83
+
84
+ function field(sourceReference, name) {
85
+ const sourceKey = referenceName(sourceReference, 'source')
86
+
87
+ if (typeof sourceReference !== 'string' && !sourceReference.fields.some((candidate) => candidate.name === name)) {
88
+ throw new TypeError(`Source ${sourceKey} does not define field ${name}`)
89
+ }
90
+
91
+ return {
92
+ kind: 'field',
93
+ source: sourceKey,
94
+ field: name,
95
+ }
96
+ }
97
+
98
+ function requiredParameter(name, scalarType) {
99
+ return parameterDefinition(name, scalarType, true)
100
+ }
101
+
102
+ function optionalParameter(name, scalarType) {
103
+ return parameterDefinition(name, scalarType, false)
104
+ }
105
+
106
+ function requiredListParameter(name, scalarType) {
107
+ return parameterDefinition(name, scalarType, true, 'list')
108
+ }
109
+
110
+ function optionalListParameter(name, scalarType) {
111
+ return parameterDefinition(name, scalarType, false, 'list')
112
+ }
113
+
114
+ function param(parameterReference) {
115
+ if (typeof parameterReference !== 'string' && parameterReference?.shape === 'list') {
116
+ throw new TypeError(`List parameter ${parameterReference.name} cannot be used as a scalar expression`)
117
+ }
118
+
119
+ return {
120
+ kind: 'parameter',
121
+ name: referenceName(parameterReference, 'parameter'),
122
+ }
123
+ }
124
+
125
+ function literal(value) {
126
+ if (value === null) {
127
+ return literalExpression({ kind: 'null' })
128
+ }
129
+ if (typeof value === 'boolean') {
130
+ return literalExpression({ kind: 'boolean', value })
131
+ }
132
+ if (typeof value === 'string') {
133
+ return literalExpression({ kind: 'string', value })
134
+ }
135
+ if (typeof value === 'number') {
136
+ return Number.isInteger(value) ? integer(value) : decimal(value)
137
+ }
138
+
139
+ throw new TypeError('Literal must be null, boolean, string, or number')
140
+ }
141
+
142
+ function integer(value) {
143
+ if (!Number.isSafeInteger(value)) {
144
+ throw new TypeError('Integer literal must be a safe JavaScript integer')
145
+ }
146
+ return literalExpression({ kind: 'integer', value })
147
+ }
148
+
149
+ function decimal(value) {
150
+ const text = String(value)
151
+ if (!/^-?(?:\d+(?:\.\d*)?|\.\d+)$/.test(text)) {
152
+ throw new TypeError(`Invalid decimal literal ${text}`)
153
+ }
154
+ return literalExpression({ kind: 'decimal', value: text })
155
+ }
156
+
157
+ function eq(left, right) {
158
+ return binaryExpression('eq', left, right)
159
+ }
160
+
161
+ function neq(left, right) {
162
+ return binaryExpression('notEq', left, right)
163
+ }
164
+
165
+ function lt(left, right) {
166
+ return binaryExpression('lessThan', left, right)
167
+ }
168
+
169
+ function lte(left, right) {
170
+ return binaryExpression('lessThanOrEqual', left, right)
171
+ }
172
+
173
+ function gt(left, right) {
174
+ return binaryExpression('greaterThan', left, right)
175
+ }
176
+
177
+ function gte(left, right) {
178
+ return binaryExpression('greaterThanOrEqual', left, right)
179
+ }
180
+
181
+ function like(expression, pattern) {
182
+ return {
183
+ kind: 'like',
184
+ expression: asExpression(expression),
185
+ pattern: asExpression(pattern),
186
+ }
187
+ }
188
+
189
+ function inList(expression, values) {
190
+ return {
191
+ kind: 'in',
192
+ expression: asExpression(expression),
193
+ values: values.map(asExpression),
194
+ }
195
+ }
196
+
197
+ function inParameter(expression, parameterReference) {
198
+ if (typeof parameterReference !== 'string' && parameterReference?.shape !== 'list') {
199
+ throw new TypeError(`Parameter ${parameterReference?.name ?? '<unknown>'} is not a list parameter`)
200
+ }
201
+
202
+ return {
203
+ kind: 'inParameter',
204
+ expression: asExpression(expression),
205
+ parameter: referenceName(parameterReference, 'parameter'),
206
+ }
207
+ }
208
+ function and(...expressions) {
209
+ return {
210
+ kind: 'and',
211
+ expressions: expressions.map(asExpression),
212
+ }
213
+ }
214
+
215
+ function or(...expressions) {
216
+ return {
217
+ kind: 'or',
218
+ expressions: expressions.map(asExpression),
219
+ }
220
+ }
221
+
222
+ function not(expression) {
223
+ return unaryExpression('not', expression)
224
+ }
225
+
226
+ function isNull(expression) {
227
+ return unaryExpression('isNull', expression)
228
+ }
229
+
230
+ function isNotNull(expression) {
231
+ return unaryExpression('isNotNull', expression)
232
+ }
233
+
234
+ function exists(sourceReference, predicate, options) {
235
+ const expression = {
236
+ kind: 'exists',
237
+ source: referenceName(sourceReference, 'source'),
238
+ }
239
+
240
+ if (options !== undefined) {
241
+ const configuration = configurationOf('exists', options, EXISTS_CONFIGURATION_KEYS)
242
+ if (configuration.from === undefined) {
243
+ throw new TypeError('exists requires configuration field "from"')
244
+ }
245
+ expression.from = referenceName(configuration.from, 'source')
246
+ }
247
+
248
+ if (predicate !== undefined) {
249
+ expression.predicate = asExpression(predicate)
250
+ }
251
+
252
+ return expression
253
+ }
254
+
255
+ function call(name, ...arguments_) {
256
+ return {
257
+ kind: 'function',
258
+ name,
259
+ arguments: arguments_.map(asExpression),
260
+ }
261
+ }
262
+
263
+ function lower(expression) {
264
+ return call('lower', expression)
265
+ }
266
+
267
+ function upper(expression) {
268
+ return call('upper', expression)
269
+ }
270
+
271
+ function coalesce(first, second, ...rest) {
272
+ return call('coalesce', first, second, ...rest)
273
+ }
274
+
275
+ function concat(first, ...rest) {
276
+ return call('concat', first, ...rest)
277
+ }
278
+
279
+ function aggregate(functionName, expression) {
280
+ const definition = {
281
+ kind: 'aggregate',
282
+ function: functionName,
283
+ }
284
+ if (expression !== undefined) {
285
+ definition.expression = asExpression(expression)
286
+ }
287
+ return definition
288
+ }
289
+
290
+ function count(expression) {
291
+ return aggregate('count', expression)
292
+ }
293
+
294
+ function countDistinct(expression) {
295
+ return aggregate('countDistinct', expression)
296
+ }
297
+
298
+ function sum(expression) {
299
+ return aggregate('sum', expression)
300
+ }
301
+
302
+ function average(expression) {
303
+ return aggregate('average', expression)
304
+ }
305
+
306
+ function minimum(expression) {
307
+ return aggregate('minimum', expression)
308
+ }
309
+
310
+ function maximum(expression) {
311
+ return aggregate('maximum', expression)
312
+ }
313
+
314
+ function relation(value) {
315
+ const configuration = configurationOf('relation', value, RELATION_CONFIGURATION_KEYS)
316
+ const definition = {
317
+ name: configuration.name,
318
+ from: referenceName(configuration.from, 'source'),
319
+ to: referenceName(configuration.to, 'source'),
320
+ on: asExpression(configuration.on),
321
+ }
322
+ if (configuration.required === true) {
323
+ definition.required = true
324
+ }
325
+ if (configuration.cardinality === 'many') {
326
+ definition.cardinality = 'many'
327
+ }
328
+ if (configuration.selection !== undefined) {
329
+ definition.selection = configuration.selection
330
+ }
331
+ return definition
332
+ }
333
+
334
+ function constraint(value) {
335
+ const configuration = configurationOf('constraint', value, CONSTRAINT_CONFIGURATION_KEYS)
336
+ const definition = {
337
+ predicate: asExpression(configuration.predicate),
338
+ }
339
+ if (configuration.when !== undefined) {
340
+ definition.when = {
341
+ kind: 'parameterPresent',
342
+ parameter: referenceName(configuration.when, 'parameter'),
343
+ }
344
+ }
345
+ return definition
346
+ }
347
+
348
+ function project(value) {
349
+ const configuration = configurationOf('project', value, PROJECTION_CONFIGURATION_KEYS)
350
+ const definition = {
351
+ path: typeof configuration.path === 'string' ? configuration.path.split('.') : [...configuration.path],
352
+ expression: asExpression(configuration.expression),
353
+ }
354
+ if (configuration.default === true) {
355
+ definition.selectedByDefault = true
356
+ }
357
+ return definition
358
+ }
359
+
360
+ function dimension(value) {
361
+ return summaryField('dimension', configurationOf('dimension', value, PROJECTION_CONFIGURATION_KEYS))
362
+ }
363
+
364
+ function measure(value) {
365
+ return summaryField('measure', configurationOf('measure', value, PROJECTION_CONFIGURATION_KEYS))
366
+ }
367
+
368
+ function summaryField(role, configuration) {
369
+ const definition = {
370
+ ...project(configuration),
371
+ role,
372
+ }
373
+ Object.defineProperty(definition, SUMMARY_FIELD_BRAND, {
374
+ value: true,
375
+ })
376
+
377
+ return definition
378
+ }
379
+
380
+ function asc(expression, options = {}) {
381
+ return orderBy('asc', expression, options)
382
+ }
383
+
384
+ function desc(expression, options = {}) {
385
+ return orderBy('desc', expression, options)
386
+ }
387
+
388
+ function ordering(value) {
389
+ const configuration = configurationOf('ordering', value, ORDERING_CONFIGURATION_KEYS)
390
+ if (typeof configuration.name !== 'string' || configuration.name.trim() === '') {
391
+ throw new TypeError('ordering name must be a non-empty string')
392
+ }
393
+ if (!Array.isArray(configuration.by) || configuration.by.length === 0) {
394
+ throw new TypeError('ordering by must contain at least one order expression')
395
+ }
396
+ if (configuration.default !== undefined && typeof configuration.default !== 'boolean') {
397
+ throw new TypeError('ordering default must be a boolean')
398
+ }
399
+
400
+ const definition = {
401
+ name: configuration.name,
402
+ orderBy: [...configuration.by],
403
+ }
404
+ if (configuration.default === true) {
405
+ definition.default = true
406
+ }
407
+
408
+ return definition
409
+ }
410
+
411
+ function firstBy(firstOrder, ...rest) {
412
+ if (firstOrder === undefined) {
413
+ throw new TypeError('firstBy requires at least one order expression')
414
+ }
415
+
416
+ return {
417
+ kind: 'firstBy',
418
+ orderBy: [firstOrder, ...rest],
419
+ }
420
+ }
421
+
422
+ function defineGraphModule(configuration) {
423
+ const content = composeDefinitionContent(configuration)
424
+
425
+ const module = {
426
+ name: configuration.name,
427
+ sources: content.sources,
428
+ parameters: content.parameters,
429
+ relations: content.relations,
430
+ constraints: content.constraints,
431
+ projection: content.projection,
432
+ orderings: content.orderings,
433
+ }
434
+ Object.defineProperty(module, GRAPH_MODULE_BRAND, { value: true })
435
+
436
+ return deepFreeze(module)
437
+ }
438
+
439
+ function defineGraph(configuration) {
440
+ return buildGraphDefinition(configuration, 'record')
441
+ }
442
+
443
+ function defineSummaryGraph(configuration) {
444
+ if (configuration.projection !== undefined) {
445
+ throw new TypeError('Summary graph uses dimensions and measures instead of projection')
446
+ }
447
+
448
+ return buildGraphDefinition(
449
+ {
450
+ ...configuration,
451
+ projection: [...(configuration.dimensions ?? []), ...(configuration.measures ?? [])],
452
+ },
453
+ 'summary',
454
+ )
455
+ }
456
+
457
+ function buildGraphDefinition(configuration, mode) {
458
+ const content = composeDefinitionContent(configuration)
459
+ validateProjectionMode(content.projection, mode)
460
+
461
+ return deepFreeze({
462
+ schemaVersion: GRAPH_DEFINITION_VERSION,
463
+ name: configuration.name,
464
+ root: referenceName(configuration.root, 'source'),
465
+ sources: content.sources.map(copySource),
466
+ parameters: content.parameters,
467
+ relations: content.relations,
468
+ constraints: content.constraints,
469
+ projection: {
470
+ fields: content.projection,
471
+ },
472
+ orderings: content.orderings,
473
+ })
474
+ }
475
+
476
+ function validateProjectionMode(projection, mode) {
477
+ if (mode === 'summary') {
478
+ if (projection.length === 0) {
479
+ throw new TypeError('Summary graph must define at least one dimension or measure')
480
+ }
481
+ const invalid = projection.find((field) => field.role !== 'dimension' && field.role !== 'measure')
482
+ if (invalid !== undefined) {
483
+ throw new TypeError(
484
+ `Summary graph projection field ${JSON.stringify(invalid.path.join('.'))} has no summary role`,
485
+ )
486
+ }
487
+ return
488
+ }
489
+
490
+ const summaryField = projection.find((field) => field.role === 'dimension' || field.role === 'measure')
491
+ if (summaryField !== undefined) {
492
+ throw new TypeError(
493
+ `Record graph projection field ${JSON.stringify(summaryField.path.join('.'))} has a summary role`,
494
+ )
495
+ }
496
+ }
497
+
498
+ function composeDefinitionContent(configuration) {
499
+ const content = {
500
+ sources: [],
501
+ parameters: [],
502
+ relations: [],
503
+ constraints: [],
504
+ projection: [],
505
+ orderings: [],
506
+ }
507
+ const indexes = {
508
+ sources: new Map(),
509
+ parameters: new Map(),
510
+ relations: new Map(),
511
+ constraints: new Set(),
512
+ projection: new Map(),
513
+ orderings: new Map(),
514
+ }
515
+
516
+ for (const [index, module] of (configuration.modules ?? []).entries()) {
517
+ if (module?.[GRAPH_MODULE_BRAND] !== true) {
518
+ throw new TypeError(`Graph module at index ${index} was not created by defineGraphModule`)
519
+ }
520
+
521
+ addDefinitionPart(content, indexes, module, `graph module ${JSON.stringify(module.name)}`)
522
+ }
523
+
524
+ addDefinitionPart(content, indexes, configuration, `definition ${JSON.stringify(configuration.name)}`)
525
+ return content
526
+ }
527
+
528
+ function addDefinitionPart(content, indexes, part, owner) {
529
+ addNamedDefinitions(content.sources, indexes.sources, part.sources, 'source', owner, (source) => source.key)
530
+ addNamedDefinitions(
531
+ content.parameters,
532
+ indexes.parameters,
533
+ part.parameters,
534
+ 'parameter',
535
+ owner,
536
+ (parameter) => parameter.name,
537
+ )
538
+ addNamedDefinitions(
539
+ content.relations,
540
+ indexes.relations,
541
+ part.relations,
542
+ 'relation',
543
+ owner,
544
+ (relation) => relation.name,
545
+ )
546
+ addIdentityDefinitions(content.constraints, indexes.constraints, part.constraints)
547
+ addNamedDefinitions(content.projection, indexes.projection, part.projection, 'projection', owner, (projection) =>
548
+ projection.path.join('.'),
549
+ )
550
+
551
+ addNamedDefinitions(
552
+ content.orderings,
553
+ indexes.orderings,
554
+ part.orderings,
555
+ 'ordering',
556
+ owner,
557
+ (orderingDefinition) => orderingDefinition.name,
558
+ )
559
+ }
560
+
561
+ function addNamedDefinitions(target, index, definitions, type, owner, getKey) {
562
+ for (const definition of definitions ?? []) {
563
+ const key = getKey(definition)
564
+ const existing = index.get(key)
565
+
566
+ if (existing === undefined) {
567
+ index.set(key, { definition, owner })
568
+ target.push(definition)
569
+ continue
570
+ }
571
+
572
+ if (existing.definition !== definition) {
573
+ throw new TypeError(`Conflicting ${type} ${JSON.stringify(key)} from ${existing.owner} and ${owner}`)
574
+ }
575
+ }
576
+ }
577
+
578
+ function addIdentityDefinitions(target, index, definitions) {
579
+ for (const definition of definitions ?? []) {
580
+ if (!index.has(definition)) {
581
+ index.add(definition)
582
+ target.push(definition)
583
+ }
584
+ }
585
+ }
586
+
587
+ function normalizeFieldSpec(specification) {
588
+ if (typeof specification === 'string') {
589
+ return fieldType(specification)
590
+ }
591
+ if (specification && typeof specification === 'object') {
592
+ return fieldType(specification.scalarType, specification)
593
+ }
594
+ throw new TypeError('Field specification must contain a scalar type')
595
+ }
596
+
597
+ function parameterDefinition(name, scalarType, required, shape = 'scalar') {
598
+ assertScalarType(scalarType)
599
+ const definition = { name, scalarType }
600
+ if (required) {
601
+ definition.required = true
602
+ }
603
+ if (shape === 'list') {
604
+ definition.shape = 'list'
605
+ }
606
+ return definition
607
+ }
608
+
609
+ function literalExpression(value) {
610
+ return {
611
+ kind: 'literal',
612
+ value,
613
+ }
614
+ }
615
+
616
+ function binaryExpression(kind, left, right) {
617
+ return {
618
+ kind,
619
+ left: asExpression(left),
620
+ right: asExpression(right),
621
+ }
622
+ }
623
+
624
+ function unaryExpression(kind, expression) {
625
+ return {
626
+ kind,
627
+ expression: asExpression(expression),
628
+ }
629
+ }
630
+
631
+ function asExpression(value) {
632
+ if (value?.[SUMMARY_FIELD_BRAND] === true) {
633
+ return value.expression
634
+ }
635
+ if (value && typeof value === 'object' && typeof value.kind === 'string') {
636
+ return value
637
+ }
638
+ return literal(value)
639
+ }
640
+
641
+ function orderBy(direction, expression, options) {
642
+ const definition = {
643
+ expression: asExpression(expression),
644
+ direction,
645
+ }
646
+ if (options.nulls !== undefined) {
647
+ definition.nulls = options.nulls
648
+ }
649
+ return definition
650
+ }
651
+
652
+ function referenceName(reference, type) {
653
+ if (typeof reference === 'string') {
654
+ return reference
655
+ }
656
+ if (type === 'source') {
657
+ if (reference && typeof reference.key === 'string') {
658
+ return reference.key
659
+ }
660
+ throw new TypeError('Invalid source reference')
661
+ }
662
+ if (reference && typeof reference.name === 'string') {
663
+ return reference.name
664
+ }
665
+ throw new TypeError(`Invalid ${type} reference`)
666
+ }
667
+
668
+ function copySource(sourceReference) {
669
+ return {
670
+ key: sourceReference.key,
671
+ fields: sourceReference.fields.map((fieldDefinition) => ({ ...fieldDefinition })),
672
+ }
673
+ }
674
+
675
+ function deepFreeze(value, seen = new WeakSet()) {
676
+ if (value === null || typeof value !== 'object' || seen.has(value)) {
677
+ return value
678
+ }
679
+
680
+ seen.add(value)
681
+ for (const property of Reflect.ownKeys(value)) {
682
+ deepFreeze(value[property], seen)
683
+ }
684
+
685
+ return Object.freeze(value)
686
+ }
687
+
688
+ function assertScalarType(scalarType) {
689
+ if (!SCALAR_TYPES.has(scalarType)) {
690
+ throw new TypeError(`Unknown scalar type ${scalarType}`)
691
+ }
692
+ }
693
+
694
+ exports.fieldType = fieldType
695
+ exports.nullable = nullable
696
+ exports.hidden = hidden
697
+ exports.source = source
698
+ exports.requiredParameter = requiredParameter
699
+ exports.optionalParameter = optionalParameter
700
+ exports.param = param
701
+ exports.literal = literal
702
+ exports.integer = integer
703
+ exports.decimal = decimal
704
+ exports.requiredListParameter = requiredListParameter
705
+ exports.optionalListParameter = optionalListParameter
706
+ exports.eq = eq
707
+ exports.neq = neq
708
+ exports.lt = lt
709
+ exports.lte = lte
710
+ exports.gt = gt
711
+ exports.gte = gte
712
+ exports.like = like
713
+ exports.inList = inList
714
+ exports.and = and
715
+ exports.or = or
716
+ exports.not = not
717
+ exports.isNull = isNull
718
+ exports.isNotNull = isNotNull
719
+ exports.inParameter = inParameter
720
+ exports.exists = exists
721
+ exports.lower = lower
722
+ exports.upper = upper
723
+ exports.coalesce = coalesce
724
+ exports.concat = concat
725
+ exports.count = count
726
+ exports.countDistinct = countDistinct
727
+ exports.sum = sum
728
+ exports.average = average
729
+ exports.minimum = minimum
730
+ exports.maximum = maximum
731
+ exports.relation = relation
732
+ exports.constraint = constraint
733
+ exports.ordering = ordering
734
+ exports.project = project
735
+ exports.dimension = dimension
736
+ exports.measure = measure
737
+ exports.asc = asc
738
+ exports.desc = desc
739
+ exports.defineGraphModule = defineGraphModule
740
+ exports.defineGraph = defineGraph
741
+ exports.defineSummaryGraph = defineSummaryGraph
742
+ exports.firstBy = firstBy