@query-graph/core 1.1.3 → 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,433 @@
1
+ 'use strict'
2
+
3
+ const { arrayOf, configurationOf, nonEmptyString, objectOf, optionalBoolean } = require('../configuration.js')
4
+ const { SUMMARY_FIELD_BRAND } = require('./symbols.js')
5
+
6
+ const SCALAR_TYPES = new Set([
7
+ 'boolean',
8
+ 'int32',
9
+ 'int64',
10
+ 'float64',
11
+ 'decimal',
12
+ 'string',
13
+ 'date',
14
+ 'dateTime',
15
+ 'binary',
16
+ 'json',
17
+ ])
18
+ const FIELD_TYPE_OPTIONS_KEYS = new Set(['nullable', 'selectable'])
19
+ const FIELD_SPECIFICATION_KEYS = new Set(['scalarType', 'nullable', 'selectable'])
20
+ const EXISTS_CONFIGURATION_KEYS = new Set(['from'])
21
+
22
+ function fieldType(scalarType, options = {}) {
23
+ assertScalarType(scalarType)
24
+ const configuration = configurationOf('fieldType', options, FIELD_TYPE_OPTIONS_KEYS)
25
+ optionalBoolean(configuration.nullable, 'fieldType nullable')
26
+ optionalBoolean(configuration.selectable, 'fieldType selectable')
27
+
28
+ const definition = { scalarType }
29
+ if (configuration.nullable === true) {
30
+ definition.nullable = true
31
+ }
32
+ if (configuration.selectable === false) {
33
+ definition.selectable = false
34
+ }
35
+ return definition
36
+ }
37
+
38
+ function nullable(specification) {
39
+ return {
40
+ ...normalizeFieldSpec(specification),
41
+ nullable: true,
42
+ }
43
+ }
44
+
45
+ function hidden(specification) {
46
+ return {
47
+ ...normalizeFieldSpec(specification),
48
+ selectable: false,
49
+ }
50
+ }
51
+
52
+ function source(key, fieldSpecifications) {
53
+ nonEmptyString(key, 'source key')
54
+ objectOf('source fields', fieldSpecifications)
55
+
56
+ const fields = Object.entries(fieldSpecifications).map(([name, specification]) => ({
57
+ name: nonEmptyString(name, 'source field name'),
58
+ ...normalizeFieldSpec(specification),
59
+ }))
60
+ const sourceReference = { key, fields }
61
+
62
+ Object.defineProperty(sourceReference, 'field', {
63
+ enumerable: false,
64
+ value(name) {
65
+ return field(sourceReference, name)
66
+ },
67
+ })
68
+
69
+ return sourceReference
70
+ }
71
+
72
+ function field(sourceReference, name) {
73
+ const sourceKey = referenceName(sourceReference, 'source')
74
+ nonEmptyString(name, 'field name')
75
+
76
+ if (typeof sourceReference !== 'string' && !sourceReference.fields.some((candidate) => candidate.name === name)) {
77
+ throw new TypeError(`Source ${sourceKey} does not define field ${name}`)
78
+ }
79
+
80
+ return {
81
+ kind: 'field',
82
+ source: sourceKey,
83
+ field: name,
84
+ }
85
+ }
86
+
87
+ function requiredParameter(name, scalarType) {
88
+ return parameterDefinition(name, scalarType, true)
89
+ }
90
+
91
+ function optionalParameter(name, scalarType) {
92
+ return parameterDefinition(name, scalarType, false)
93
+ }
94
+
95
+ function requiredListParameter(name, scalarType) {
96
+ return parameterDefinition(name, scalarType, true, 'list')
97
+ }
98
+
99
+ function optionalListParameter(name, scalarType) {
100
+ return parameterDefinition(name, scalarType, false, 'list')
101
+ }
102
+
103
+ function param(parameterReference) {
104
+ if (typeof parameterReference !== 'string' && parameterReference?.shape === 'list') {
105
+ throw new TypeError(`List parameter ${parameterReference.name} cannot be used as a scalar expression`)
106
+ }
107
+
108
+ return {
109
+ kind: 'parameter',
110
+ name: referenceName(parameterReference, 'parameter'),
111
+ }
112
+ }
113
+
114
+ function literal(value) {
115
+ if (value === null) {
116
+ return literalExpression({ kind: 'null' })
117
+ }
118
+ if (typeof value === 'boolean') {
119
+ return literalExpression({ kind: 'boolean', value })
120
+ }
121
+ if (typeof value === 'string') {
122
+ return literalExpression({ kind: 'string', value })
123
+ }
124
+ if (typeof value === 'number') {
125
+ return Number.isInteger(value) ? integer(value) : decimal(value)
126
+ }
127
+
128
+ throw new TypeError('Literal must be null, boolean, string, or number')
129
+ }
130
+
131
+ function integer(value) {
132
+ if (!Number.isSafeInteger(value)) {
133
+ throw new TypeError('Integer literal must be a safe JavaScript integer')
134
+ }
135
+ return literalExpression({ kind: 'integer', value })
136
+ }
137
+
138
+ function decimal(value) {
139
+ const text = String(value)
140
+ if (!/^-?(?:\d+(?:\.\d*)?|\.\d+)$/.test(text)) {
141
+ throw new TypeError(`Invalid decimal literal ${text}`)
142
+ }
143
+ return literalExpression({ kind: 'decimal', value: text })
144
+ }
145
+
146
+ function eq(left, right) {
147
+ return binaryExpression('eq', left, right)
148
+ }
149
+
150
+ function neq(left, right) {
151
+ return binaryExpression('notEq', left, right)
152
+ }
153
+
154
+ function lt(left, right) {
155
+ return binaryExpression('lessThan', left, right)
156
+ }
157
+
158
+ function lte(left, right) {
159
+ return binaryExpression('lessThanOrEqual', left, right)
160
+ }
161
+
162
+ function gt(left, right) {
163
+ return binaryExpression('greaterThan', left, right)
164
+ }
165
+
166
+ function gte(left, right) {
167
+ return binaryExpression('greaterThanOrEqual', left, right)
168
+ }
169
+
170
+ function like(expression, pattern) {
171
+ return {
172
+ kind: 'like',
173
+ expression: asExpression(expression),
174
+ pattern: asExpression(pattern),
175
+ }
176
+ }
177
+
178
+ function inList(expression, values) {
179
+ return {
180
+ kind: 'in',
181
+ expression: asExpression(expression),
182
+ values: arrayOf(values, 'inList values').map(asExpression),
183
+ }
184
+ }
185
+
186
+ function inParameter(expression, parameterReference) {
187
+ if (typeof parameterReference !== 'string' && parameterReference?.shape !== 'list') {
188
+ throw new TypeError(`Parameter ${parameterReference?.name ?? '<unknown>'} is not a list parameter`)
189
+ }
190
+
191
+ return {
192
+ kind: 'inParameter',
193
+ expression: asExpression(expression),
194
+ parameter: referenceName(parameterReference, 'parameter'),
195
+ }
196
+ }
197
+
198
+ function and(...expressions) {
199
+ return {
200
+ kind: 'and',
201
+ expressions: expressions.map(asExpression),
202
+ }
203
+ }
204
+
205
+ function or(...expressions) {
206
+ return {
207
+ kind: 'or',
208
+ expressions: expressions.map(asExpression),
209
+ }
210
+ }
211
+
212
+ function not(expression) {
213
+ return unaryExpression('not', expression)
214
+ }
215
+
216
+ function isNull(expression) {
217
+ return unaryExpression('isNull', expression)
218
+ }
219
+
220
+ function isNotNull(expression) {
221
+ return unaryExpression('isNotNull', expression)
222
+ }
223
+
224
+ function exists(sourceReference, predicate, options) {
225
+ const expression = {
226
+ kind: 'exists',
227
+ source: referenceName(sourceReference, 'source'),
228
+ }
229
+
230
+ if (options !== undefined) {
231
+ const configuration = configurationOf('exists', options, EXISTS_CONFIGURATION_KEYS)
232
+ if (configuration.from === undefined) {
233
+ throw new TypeError('exists requires configuration field "from"')
234
+ }
235
+ expression.from = referenceName(configuration.from, 'source')
236
+ }
237
+
238
+ if (predicate !== undefined) {
239
+ expression.predicate = asExpression(predicate)
240
+ }
241
+
242
+ return expression
243
+ }
244
+
245
+ function call(name, ...arguments_) {
246
+ return {
247
+ kind: 'function',
248
+ name,
249
+ arguments: arguments_.map(asExpression),
250
+ }
251
+ }
252
+
253
+ function lower(expression) {
254
+ return call('lower', expression)
255
+ }
256
+
257
+ function upper(expression) {
258
+ return call('upper', expression)
259
+ }
260
+
261
+ function coalesce(first, second, ...rest) {
262
+ return call('coalesce', first, second, ...rest)
263
+ }
264
+
265
+ function concat(first, ...rest) {
266
+ return call('concat', first, ...rest)
267
+ }
268
+
269
+ function aggregate(functionName, expression) {
270
+ const definition = {
271
+ kind: 'aggregate',
272
+ function: functionName,
273
+ }
274
+ if (expression !== undefined) {
275
+ definition.expression = asExpression(expression)
276
+ }
277
+ return definition
278
+ }
279
+
280
+ function count(expression) {
281
+ return aggregate('count', expression)
282
+ }
283
+
284
+ function countDistinct(expression) {
285
+ return aggregate('countDistinct', expression)
286
+ }
287
+
288
+ function sum(expression) {
289
+ return aggregate('sum', expression)
290
+ }
291
+
292
+ function average(expression) {
293
+ return aggregate('average', expression)
294
+ }
295
+
296
+ function minimum(expression) {
297
+ return aggregate('minimum', expression)
298
+ }
299
+
300
+ function maximum(expression) {
301
+ return aggregate('maximum', expression)
302
+ }
303
+
304
+ function normalizeFieldSpec(specification) {
305
+ if (typeof specification === 'string') {
306
+ return fieldType(specification)
307
+ }
308
+ if (specification && typeof specification === 'object' && !Array.isArray(specification)) {
309
+ const configuration = configurationOf('field specification', specification, FIELD_SPECIFICATION_KEYS)
310
+ return fieldType(configuration.scalarType, {
311
+ nullable: configuration.nullable,
312
+ selectable: configuration.selectable,
313
+ })
314
+ }
315
+ throw new TypeError('Field specification must contain a scalar type')
316
+ }
317
+
318
+ function parameterDefinition(name, scalarType, required, shape = 'scalar') {
319
+ nonEmptyString(name, 'parameter name')
320
+ assertScalarType(scalarType)
321
+ const definition = { name, scalarType }
322
+ if (required) {
323
+ definition.required = true
324
+ }
325
+ if (shape === 'list') {
326
+ definition.shape = 'list'
327
+ }
328
+ return definition
329
+ }
330
+
331
+ function literalExpression(value) {
332
+ return {
333
+ kind: 'literal',
334
+ value,
335
+ }
336
+ }
337
+
338
+ function binaryExpression(kind, left, right) {
339
+ return {
340
+ kind,
341
+ left: asExpression(left),
342
+ right: asExpression(right),
343
+ }
344
+ }
345
+
346
+ function unaryExpression(kind, expression) {
347
+ return {
348
+ kind,
349
+ expression: asExpression(expression),
350
+ }
351
+ }
352
+
353
+ function asExpression(value) {
354
+ if (value?.[SUMMARY_FIELD_BRAND] === true) {
355
+ return value.expression
356
+ }
357
+ if (value && typeof value === 'object' && typeof value.kind === 'string') {
358
+ return value
359
+ }
360
+ return literal(value)
361
+ }
362
+
363
+ function referenceName(reference, type) {
364
+ if (typeof reference === 'string') {
365
+ return nonEmptyString(reference, `${type} name`)
366
+ }
367
+ if (type === 'source') {
368
+ if (reference && typeof reference.key === 'string') {
369
+ return nonEmptyString(reference.key, 'source key')
370
+ }
371
+ throw new TypeError('Invalid source reference')
372
+ }
373
+ if (reference && typeof reference.name === 'string') {
374
+ return nonEmptyString(reference.name, `${type} name`)
375
+ }
376
+ throw new TypeError(`Invalid ${type} reference`)
377
+ }
378
+
379
+ function copySource(sourceReference) {
380
+ return {
381
+ key: sourceReference.key,
382
+ fields: sourceReference.fields.map((fieldDefinition) => ({ ...fieldDefinition })),
383
+ }
384
+ }
385
+
386
+ function assertScalarType(scalarType) {
387
+ if (!SCALAR_TYPES.has(scalarType)) {
388
+ throw new TypeError(`Unknown scalar type ${scalarType}`)
389
+ }
390
+ }
391
+
392
+ module.exports = {
393
+ and,
394
+ asExpression,
395
+ average,
396
+ coalesce,
397
+ concat,
398
+ copySource,
399
+ count,
400
+ countDistinct,
401
+ decimal,
402
+ eq,
403
+ exists,
404
+ fieldType,
405
+ gt,
406
+ gte,
407
+ hidden,
408
+ inList,
409
+ inParameter,
410
+ integer,
411
+ isNotNull,
412
+ isNull,
413
+ like,
414
+ literal,
415
+ lower,
416
+ lt,
417
+ lte,
418
+ maximum,
419
+ minimum,
420
+ neq,
421
+ not,
422
+ nullable,
423
+ optionalListParameter,
424
+ optionalParameter,
425
+ or,
426
+ param,
427
+ referenceName,
428
+ requiredListParameter,
429
+ requiredParameter,
430
+ source,
431
+ sum,
432
+ upper,
433
+ }
@@ -0,0 +1,4 @@
1
+ 'use strict'
2
+
3
+ exports.GRAPH_MODULE_BRAND = Symbol('GraphModule')
4
+ exports.SUMMARY_FIELD_BRAND = Symbol('SummaryField')
@@ -1,34 +1,15 @@
1
1
  'use strict'
2
2
 
3
+ const { arrayOf, configurationOf, nonEmptyString, objectOf, optionalEnum } = require('./configuration.js')
4
+
3
5
  const BATCH_QUERY_KEYS = new Set(['graph', 'key'])
4
6
  const BATCH_QUERY_KEY_KEYS = new Set(['path', 'parameter'])
5
7
  const BATCH_RELATION_KEYS = new Set(['name', 'from', 'query', 'cardinality', 'parameters', 'ordering'])
6
8
  const COMPOSE_KEYS = new Set(['root', 'relations'])
9
+ const BATCH_CARDINALITIES = new Set(['one', 'many'])
7
10
  const batchQueryBrand = Symbol('BatchQuery')
8
11
  const batchBrand = Symbol('BatchRelation')
9
12
 
10
- function configurationOf(factory, value, allowedKeys) {
11
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
12
- throw new TypeError(`${factory} expects a configuration object`)
13
- }
14
-
15
- const unknownKeys = Reflect.ownKeys(value).filter((key) => typeof key !== 'string' || !allowedKeys.has(key))
16
- if (unknownKeys.length > 0) {
17
- const label = unknownKeys.length === 1 ? 'field' : 'fields'
18
- const keys = unknownKeys.map((key) => JSON.stringify(String(key))).join(', ')
19
- throw new TypeError(`${factory} received unknown configuration ${label} ${keys}`)
20
- }
21
-
22
- return value
23
- }
24
-
25
- function nonEmptyString(value, label) {
26
- if (typeof value !== 'string' || value.trim() === '') {
27
- throw new TypeError(`${label} must be a non-empty string`)
28
- }
29
- return value
30
- }
31
-
32
13
  function relationalGraph(value, label) {
33
14
  if (value === null || typeof value !== 'object' || typeof value.compose !== 'function') {
34
15
  throw new TypeError(`${label} must be a RelationalQueryGraph`)
@@ -72,17 +53,12 @@ function batchRelation(value) {
72
53
 
73
54
  const from = nonEmptyString(configuration.from, 'batchRelation.from')
74
55
  const query = batchQueryOf(configuration.query, 'batchRelation.query')
75
-
76
- if (configuration.cardinality !== 'one' && configuration.cardinality !== 'many') {
56
+ optionalEnum(configuration.cardinality, BATCH_CARDINALITIES, 'batchRelation.cardinality')
57
+ if (configuration.cardinality === undefined) {
77
58
  throw new TypeError("batchRelation.cardinality must be 'one' or 'many'")
78
59
  }
79
- if (
80
- configuration.parameters !== undefined &&
81
- (configuration.parameters === null ||
82
- typeof configuration.parameters !== 'object' ||
83
- Array.isArray(configuration.parameters))
84
- ) {
85
- throw new TypeError('batchRelation.parameters must be an object')
60
+ if (configuration.parameters !== undefined) {
61
+ objectOf('batchRelation.parameters', configuration.parameters)
86
62
  }
87
63
  if (configuration.ordering !== undefined) {
88
64
  nonEmptyString(configuration.ordering, 'batchRelation.ordering')
@@ -102,11 +78,9 @@ function batchRelation(value) {
102
78
  function composeGraph(value) {
103
79
  const configuration = configurationOf('composeGraph', value, COMPOSE_KEYS)
104
80
  const root = relationalGraph(configuration.root, 'composeGraph.root')
105
- if (!Array.isArray(configuration.relations)) {
106
- throw new TypeError('composeGraph.relations must be an array')
107
- }
81
+ const relations = arrayOf(configuration.relations, 'composeGraph.relations')
108
82
 
109
- return configuration.relations.reduce((graph, relation, index) => {
83
+ return relations.reduce((graph, relation, index) => {
110
84
  if (relation?.[batchBrand] !== true) {
111
85
  throw new TypeError(`composeGraph.relations[${index}] must be created by batchRelation`)
112
86
  }
@@ -0,0 +1,66 @@
1
+ 'use strict'
2
+
3
+ function objectOf(label, value) {
4
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
5
+ throw new TypeError(`${label} must be an object`)
6
+ }
7
+ return value
8
+ }
9
+
10
+ function configurationOf(factory, value, allowedKeys) {
11
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
12
+ throw new TypeError(`${factory} expects a configuration object`)
13
+ }
14
+
15
+ const unknownKeys = Reflect.ownKeys(value).filter((key) => typeof key !== 'string' || !allowedKeys.has(key))
16
+ if (unknownKeys.length > 0) {
17
+ const label = unknownKeys.length === 1 ? 'field' : 'fields'
18
+ const keys = unknownKeys.map((key) => JSON.stringify(String(key))).join(', ')
19
+ throw new TypeError(`${factory} received unknown configuration ${label} ${keys}`)
20
+ }
21
+
22
+ return value
23
+ }
24
+
25
+ function nonEmptyString(value, label) {
26
+ if (typeof value !== 'string' || value.trim() === '') {
27
+ throw new TypeError(`${label} must be a non-empty string`)
28
+ }
29
+ return value
30
+ }
31
+
32
+ function optionalBoolean(value, label) {
33
+ if (value !== undefined && typeof value !== 'boolean') {
34
+ throw new TypeError(`${label} must be a boolean`)
35
+ }
36
+ return value
37
+ }
38
+
39
+ function optionalEnum(value, allowedValues, label) {
40
+ if (value !== undefined && !allowedValues.has(value)) {
41
+ const choices = [...allowedValues].map((choice) => JSON.stringify(choice)).join(' or ')
42
+ throw new TypeError(`${label} must be ${choices}`)
43
+ }
44
+ return value
45
+ }
46
+
47
+ function arrayOf(value, label) {
48
+ if (!Array.isArray(value)) {
49
+ throw new TypeError(`${label} must be an array`)
50
+ }
51
+ return value
52
+ }
53
+
54
+ function optionalArray(value, label) {
55
+ return value === undefined ? undefined : arrayOf(value, label)
56
+ }
57
+
58
+ module.exports = {
59
+ arrayOf,
60
+ configurationOf,
61
+ nonEmptyString,
62
+ objectOf,
63
+ optionalArray,
64
+ optionalBoolean,
65
+ optionalEnum,
66
+ }
package/native.d.ts ADDED
@@ -0,0 +1,98 @@
1
+ /* auto-generated by NAPI-RS */
2
+ /* eslint-disable */
3
+ export declare class CompiledQueryPlan {
4
+ get root(): CompiledSqlStatement
5
+ get batches(): Array<CompiledBatchStep>
6
+ compileBatch(name: string, keys: readonly unknown[]): CompiledSqlStatement
7
+ }
8
+
9
+ export declare class ComposedQueryGraph {
10
+ get name(): string
11
+ withBatchRelation(graph: RelationalQueryGraph, relation: import('./dsl.js').BatchRelationWire): ComposedQueryGraph
12
+ compileSqlServerPlan(
13
+ operation: import('./dsl.js').QueryOperation,
14
+ options?: import('./dsl.js').SqlServerCompileOptions,
15
+ ): CompiledQueryPlan
16
+ compileOraclePlan(
17
+ operation: import('./dsl.js').QueryOperation,
18
+ options?: import('./dsl.js').OracleCompileOptions,
19
+ ): CompiledQueryPlan
20
+ }
21
+
22
+ export declare class QueryGraph {
23
+ get name(): string
24
+ get root(): string
25
+ get sourceCount(): number
26
+ get relationCount(): number
27
+ hasSource(source: string): boolean
28
+ hasField(source: string, field: string): boolean
29
+ hasParameter(parameter: string): boolean
30
+ hasRelation(relation: string): boolean
31
+ selectableFields(): Array<string>
32
+ withRelationalMapping(mapping: import('./dsl.js').RelationalMapping): RelationalQueryGraph
33
+ withRelationalMappings(mappings: readonly import('./dsl.js').RelationalMapping[]): RelationalQueryGraph
34
+ }
35
+
36
+ export declare class RelationalQueryGraph {
37
+ get name(): string
38
+ compose(): ComposedQueryGraph
39
+ compileSqlServer(
40
+ operation: import('./dsl.js').QueryOperation,
41
+ options?: import('./dsl.js').SqlServerCompileOptions,
42
+ ): CompiledSqlStatement
43
+ compileOracle(
44
+ operation: import('./dsl.js').QueryOperation,
45
+ options?: import('./dsl.js').OracleCompileOptions,
46
+ ): CompiledSqlStatement
47
+ }
48
+
49
+ export interface CompiledBatchStep {
50
+ name: string
51
+ parentKey: string
52
+ childKey: string
53
+ keyParameter: string
54
+ parameters: Readonly<Record<string, unknown>>
55
+ cardinality: import('./dsl.js').BatchCardinality
56
+ parentKeyInjected: boolean
57
+ childKeyInjected: boolean
58
+ }
59
+
60
+ export interface CompiledSqlColumn {
61
+ name: string
62
+ path: string
63
+ scalarType: import('./dsl.js').ScalarType
64
+ nullable: boolean
65
+ relations: Array<string>
66
+ }
67
+
68
+ export interface CompiledSqlObject {
69
+ path: string
70
+ presenceColumn: string
71
+ }
72
+
73
+ export interface CompiledSqlRelation {
74
+ name: string
75
+ from: string
76
+ to: string
77
+ cardinality: import('./dsl.js').RelationCardinality
78
+ required: boolean
79
+ }
80
+
81
+ export interface CompiledSqlStatement {
82
+ sql: string
83
+ bindings: Array<SqlBinding>
84
+ columns: Array<CompiledSqlColumn>
85
+ objects: Array<CompiledSqlObject>
86
+ relations: Array<CompiledSqlRelation>
87
+ }
88
+
89
+ export declare function registerDefinition<const Definition extends import('./dsl.js').GraphDefinitionInput>(
90
+ definition: import('./dsl.js').ExactGraphDefinitionInput<Definition>,
91
+ ): import('./dsl.js').QueryGraph<Definition>
92
+
93
+ export interface SqlBinding {
94
+ name: string
95
+ parameter: string
96
+ scalarType: import('./dsl.js').ScalarType
97
+ index?: number
98
+ }