@query-graph/core 1.1.4 → 1.1.5

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.
@@ -0,0 +1,425 @@
1
+ 'use strict'
2
+
3
+ const {
4
+ configurationOf,
5
+ nonEmptyString,
6
+ objectOf,
7
+ optionalArray,
8
+ optionalBoolean,
9
+ optionalEnum,
10
+ } = require('../configuration.js')
11
+ const { asExpression, copySource, referenceName } = require('./primitives.js')
12
+ const { GRAPH_MODULE_BRAND, SUMMARY_FIELD_BRAND } = require('./symbols.js')
13
+
14
+ const GRAPH_DEFINITION_VERSION = 10
15
+ const RELATION_CONFIGURATION_KEYS = new Set(['name', 'from', 'to', 'on', 'required', 'cardinality', 'selection'])
16
+ const CONSTRAINT_CONFIGURATION_KEYS = new Set(['predicate', 'when'])
17
+ const PROJECTION_CONFIGURATION_KEYS = new Set(['path', 'expression', 'default'])
18
+ const PROJECTION_OBJECT_CONFIGURATION_KEYS = new Set(['path', 'presence'])
19
+ const ORDERING_CONFIGURATION_KEYS = new Set(['name', 'by', 'default'])
20
+ const ORDER_BY_OPTIONS_KEYS = new Set(['nulls'])
21
+ const GRAPH_MODULE_CONFIGURATION_KEYS = new Set([
22
+ 'name',
23
+ 'modules',
24
+ 'sources',
25
+ 'parameters',
26
+ 'relations',
27
+ 'constraints',
28
+ 'projection',
29
+ 'objects',
30
+ 'orderings',
31
+ ])
32
+ const GRAPH_CONFIGURATION_KEYS = new Set([...GRAPH_MODULE_CONFIGURATION_KEYS, 'root'])
33
+ const SUMMARY_GRAPH_CONFIGURATION_KEYS = new Set([
34
+ 'name',
35
+ 'root',
36
+ 'modules',
37
+ 'sources',
38
+ 'parameters',
39
+ 'relations',
40
+ 'constraints',
41
+ 'dimensions',
42
+ 'measures',
43
+ 'orderings',
44
+ ])
45
+ const RELATION_CARDINALITIES = new Set(['one', 'many'])
46
+ const NULLS_ORDERS = new Set(['first', 'last'])
47
+ const DEFINITION_LIST_KEYS = [
48
+ 'modules',
49
+ 'sources',
50
+ 'parameters',
51
+ 'relations',
52
+ 'constraints',
53
+ 'projection',
54
+ 'objects',
55
+ 'orderings',
56
+ ]
57
+
58
+ function relation(value) {
59
+ const configuration = configurationOf('relation', value, RELATION_CONFIGURATION_KEYS)
60
+ nonEmptyString(configuration.name, 'relation name')
61
+ optionalBoolean(configuration.required, 'relation required')
62
+ optionalEnum(configuration.cardinality, RELATION_CARDINALITIES, 'relation cardinality')
63
+
64
+ const definition = {
65
+ name: configuration.name,
66
+ from: referenceName(configuration.from, 'source'),
67
+ to: referenceName(configuration.to, 'source'),
68
+ on: asExpression(configuration.on),
69
+ }
70
+ if (configuration.required === true) {
71
+ definition.required = true
72
+ }
73
+ if (configuration.cardinality === 'many') {
74
+ definition.cardinality = 'many'
75
+ }
76
+ if (configuration.selection !== undefined) {
77
+ definition.selection = configuration.selection
78
+ }
79
+ return definition
80
+ }
81
+
82
+ function constraint(value) {
83
+ const configuration = configurationOf('constraint', value, CONSTRAINT_CONFIGURATION_KEYS)
84
+ const definition = {
85
+ predicate: asExpression(configuration.predicate),
86
+ }
87
+ if (configuration.when !== undefined) {
88
+ definition.when = {
89
+ kind: 'parameterPresent',
90
+ parameter: referenceName(configuration.when, 'parameter'),
91
+ }
92
+ }
93
+ return definition
94
+ }
95
+
96
+ function project(value) {
97
+ const configuration = configurationOf('project', value, PROJECTION_CONFIGURATION_KEYS)
98
+ optionalBoolean(configuration.default, 'project default')
99
+ const definition = {
100
+ path: projectionPath(configuration.path, 'project path'),
101
+ expression: asExpression(configuration.expression),
102
+ }
103
+ if (configuration.default === true) {
104
+ definition.selectedByDefault = true
105
+ }
106
+ return definition
107
+ }
108
+
109
+ function projectObject(value) {
110
+ const configuration = configurationOf('projectObject', value, PROJECTION_OBJECT_CONFIGURATION_KEYS)
111
+ return {
112
+ path: projectionPath(configuration.path, 'projectObject path'),
113
+ presence: asExpression(configuration.presence),
114
+ }
115
+ }
116
+
117
+ function dimension(value) {
118
+ return summaryField('dimension', configurationOf('dimension', value, PROJECTION_CONFIGURATION_KEYS))
119
+ }
120
+
121
+ function measure(value) {
122
+ return summaryField('measure', configurationOf('measure', value, PROJECTION_CONFIGURATION_KEYS))
123
+ }
124
+
125
+ function summaryField(role, configuration) {
126
+ const definition = {
127
+ ...project(configuration),
128
+ role,
129
+ }
130
+ Object.defineProperty(definition, SUMMARY_FIELD_BRAND, {
131
+ value: true,
132
+ })
133
+
134
+ return definition
135
+ }
136
+
137
+ function asc(expression, options = {}) {
138
+ return orderBy('asc', expression, options)
139
+ }
140
+
141
+ function desc(expression, options = {}) {
142
+ return orderBy('desc', expression, options)
143
+ }
144
+
145
+ function ordering(value) {
146
+ const configuration = configurationOf('ordering', value, ORDERING_CONFIGURATION_KEYS)
147
+ nonEmptyString(configuration.name, 'ordering name')
148
+ if (!Array.isArray(configuration.by) || configuration.by.length === 0) {
149
+ throw new TypeError('ordering by must contain at least one order expression')
150
+ }
151
+ optionalBoolean(configuration.default, 'ordering default')
152
+
153
+ const definition = {
154
+ name: configuration.name,
155
+ orderBy: [...configuration.by],
156
+ }
157
+ if (configuration.default === true) {
158
+ definition.default = true
159
+ }
160
+
161
+ return definition
162
+ }
163
+
164
+ function firstBy(firstOrder, ...rest) {
165
+ if (firstOrder === undefined) {
166
+ throw new TypeError('firstBy requires at least one order expression')
167
+ }
168
+
169
+ return {
170
+ kind: 'firstBy',
171
+ orderBy: [firstOrder, ...rest],
172
+ }
173
+ }
174
+
175
+ function defineGraphModule(value) {
176
+ const configuration = definitionConfiguration('defineGraphModule', value, GRAPH_MODULE_CONFIGURATION_KEYS)
177
+ const content = composeDefinitionContent(configuration)
178
+
179
+ const module = {
180
+ name: configuration.name,
181
+ sources: content.sources,
182
+ parameters: content.parameters,
183
+ relations: content.relations,
184
+ constraints: content.constraints,
185
+ projection: content.projection,
186
+ objects: content.objects,
187
+ orderings: content.orderings,
188
+ }
189
+ Object.defineProperty(module, GRAPH_MODULE_BRAND, { value: true })
190
+
191
+ return deepFreeze(module)
192
+ }
193
+
194
+ function defineGraph(value) {
195
+ const configuration = definitionConfiguration('defineGraph', value, GRAPH_CONFIGURATION_KEYS, true)
196
+ return buildGraphDefinition(configuration, 'record')
197
+ }
198
+
199
+ function defineSummaryGraph(value) {
200
+ objectOf('defineSummaryGraph configuration', value)
201
+ if (Object.prototype.hasOwnProperty.call(value, 'projection')) {
202
+ throw new TypeError('Summary graph uses dimensions and measures instead of projection')
203
+ }
204
+
205
+ const configuration = configurationOf('defineSummaryGraph', value, SUMMARY_GRAPH_CONFIGURATION_KEYS)
206
+ validateCommonDefinitionConfiguration('defineSummaryGraph', configuration, true)
207
+ optionalArray(configuration.dimensions, 'defineSummaryGraph.dimensions')
208
+ optionalArray(configuration.measures, 'defineSummaryGraph.measures')
209
+
210
+ return buildGraphDefinition(
211
+ {
212
+ ...configuration,
213
+ projection: [...(configuration.dimensions ?? []), ...(configuration.measures ?? [])],
214
+ },
215
+ 'summary',
216
+ )
217
+ }
218
+
219
+ function buildGraphDefinition(configuration, mode) {
220
+ const content = composeDefinitionContent(configuration)
221
+ validateProjectionMode(content.projection, content.objects, mode)
222
+
223
+ return deepFreeze({
224
+ schemaVersion: GRAPH_DEFINITION_VERSION,
225
+ name: configuration.name,
226
+ root: referenceName(configuration.root, 'source'),
227
+ sources: content.sources.map(copySource),
228
+ parameters: content.parameters,
229
+ relations: content.relations,
230
+ constraints: content.constraints,
231
+ projection: {
232
+ fields: content.projection,
233
+ objects: content.objects,
234
+ },
235
+ orderings: content.orderings,
236
+ })
237
+ }
238
+
239
+ function definitionConfiguration(factory, value, allowedKeys, requiresRoot = false) {
240
+ const configuration = configurationOf(factory, value, allowedKeys)
241
+ validateCommonDefinitionConfiguration(factory, configuration, requiresRoot)
242
+ return configuration
243
+ }
244
+
245
+ function validateCommonDefinitionConfiguration(factory, configuration, requiresRoot) {
246
+ nonEmptyString(configuration.name, `${factory} name`)
247
+ if (requiresRoot) {
248
+ referenceName(configuration.root, 'source')
249
+ }
250
+ for (const key of DEFINITION_LIST_KEYS) {
251
+ optionalArray(configuration[key], `${factory}.${key}`)
252
+ }
253
+ }
254
+
255
+ function validateProjectionMode(projection, objects, mode) {
256
+ if (mode === 'summary') {
257
+ if (objects.length > 0) {
258
+ throw new TypeError('Summary graph cannot define projection objects')
259
+ }
260
+ if (projection.length === 0) {
261
+ throw new TypeError('Summary graph must define at least one dimension or measure')
262
+ }
263
+ const invalid = projection.find((field) => field.role !== 'dimension' && field.role !== 'measure')
264
+ if (invalid !== undefined) {
265
+ throw new TypeError(
266
+ `Summary graph projection field ${JSON.stringify(invalid.path.join('.'))} has no summary role`,
267
+ )
268
+ }
269
+ return
270
+ }
271
+
272
+ const summaryFieldDefinition = projection.find((field) => field.role === 'dimension' || field.role === 'measure')
273
+ if (summaryFieldDefinition !== undefined) {
274
+ throw new TypeError(
275
+ `Record graph projection field ${JSON.stringify(summaryFieldDefinition.path.join('.'))} has a summary role`,
276
+ )
277
+ }
278
+ }
279
+
280
+ function composeDefinitionContent(configuration) {
281
+ const content = {
282
+ sources: [],
283
+ parameters: [],
284
+ relations: [],
285
+ constraints: [],
286
+ projection: [],
287
+ objects: [],
288
+ orderings: [],
289
+ }
290
+ const indexes = {
291
+ sources: new Map(),
292
+ parameters: new Map(),
293
+ relations: new Map(),
294
+ constraints: new Set(),
295
+ projection: new Map(),
296
+ objects: new Map(),
297
+ orderings: new Map(),
298
+ }
299
+
300
+ for (const [index, module] of (configuration.modules ?? []).entries()) {
301
+ if (module?.[GRAPH_MODULE_BRAND] !== true) {
302
+ throw new TypeError(`Graph module at index ${index} was not created by defineGraphModule`)
303
+ }
304
+
305
+ addDefinitionPart(content, indexes, module, `graph module ${JSON.stringify(module.name)}`)
306
+ }
307
+
308
+ addDefinitionPart(content, indexes, configuration, `definition ${JSON.stringify(configuration.name)}`)
309
+ return content
310
+ }
311
+
312
+ function addDefinitionPart(content, indexes, part, owner) {
313
+ addNamedDefinitions(content.sources, indexes.sources, part.sources, 'source', owner, (source) => source.key)
314
+ addNamedDefinitions(
315
+ content.parameters,
316
+ indexes.parameters,
317
+ part.parameters,
318
+ 'parameter',
319
+ owner,
320
+ (parameter) => parameter.name,
321
+ )
322
+ addNamedDefinitions(
323
+ content.relations,
324
+ indexes.relations,
325
+ part.relations,
326
+ 'relation',
327
+ owner,
328
+ (relationDefinition) => relationDefinition.name,
329
+ )
330
+ addIdentityDefinitions(content.constraints, indexes.constraints, part.constraints)
331
+ addNamedDefinitions(content.projection, indexes.projection, part.projection, 'projection', owner, (projection) =>
332
+ projection.path.join('.'),
333
+ )
334
+ addNamedDefinitions(content.objects, indexes.objects, part.objects, 'projection object', owner, (object) =>
335
+ object.path.join('.'),
336
+ )
337
+ addNamedDefinitions(
338
+ content.orderings,
339
+ indexes.orderings,
340
+ part.orderings,
341
+ 'ordering',
342
+ owner,
343
+ (orderingDefinition) => orderingDefinition.name,
344
+ )
345
+ }
346
+
347
+ function addNamedDefinitions(target, index, definitions, type, owner, getKey) {
348
+ for (const definition of definitions ?? []) {
349
+ const key = getKey(definition)
350
+ const existing = index.get(key)
351
+
352
+ if (existing === undefined) {
353
+ index.set(key, { definition, owner })
354
+ target.push(definition)
355
+ continue
356
+ }
357
+
358
+ if (existing.definition !== definition) {
359
+ throw new TypeError(`Conflicting ${type} ${JSON.stringify(key)} from ${existing.owner} and ${owner}`)
360
+ }
361
+ }
362
+ }
363
+
364
+ function addIdentityDefinitions(target, index, definitions) {
365
+ for (const definition of definitions ?? []) {
366
+ if (!index.has(definition)) {
367
+ index.add(definition)
368
+ target.push(definition)
369
+ }
370
+ }
371
+ }
372
+
373
+ function projectionPath(path, label) {
374
+ if (typeof path === 'string') {
375
+ return path.split('.')
376
+ }
377
+ if (!Array.isArray(path) || path.some((segment) => typeof segment !== 'string')) {
378
+ throw new TypeError(`${label} must be a string or an array of strings`)
379
+ }
380
+ return [...path]
381
+ }
382
+
383
+ function orderBy(direction, expression, options) {
384
+ const configuration = configurationOf(`${direction} order`, options, ORDER_BY_OPTIONS_KEYS)
385
+ optionalEnum(configuration.nulls, NULLS_ORDERS, `${direction} nulls`)
386
+
387
+ const definition = {
388
+ expression: asExpression(expression),
389
+ direction,
390
+ }
391
+ if (configuration.nulls !== undefined) {
392
+ definition.nulls = configuration.nulls
393
+ }
394
+ return definition
395
+ }
396
+
397
+ function deepFreeze(value, seen = new WeakSet()) {
398
+ if (value === null || typeof value !== 'object' || seen.has(value)) {
399
+ return value
400
+ }
401
+
402
+ seen.add(value)
403
+ for (const property of Reflect.ownKeys(value)) {
404
+ deepFreeze(value[property], seen)
405
+ }
406
+
407
+ return Object.freeze(value)
408
+ }
409
+
410
+ module.exports = {
411
+ GRAPH_DEFINITION_VERSION,
412
+ asc,
413
+ constraint,
414
+ defineGraph,
415
+ defineGraphModule,
416
+ defineSummaryGraph,
417
+ desc,
418
+ dimension,
419
+ firstBy,
420
+ measure,
421
+ ordering,
422
+ project,
423
+ projectObject,
424
+ relation,
425
+ }