jinaga-server 3.5.2 → 3.5.3
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/authorization/authorization-keystore.js +2 -2
- package/dist/authorization/authorization-keystore.js.map +1 -1
- package/dist/http/csv-metadata.d.ts +24 -0
- package/dist/http/csv-metadata.js +3 -0
- package/dist/http/csv-metadata.js.map +1 -0
- package/dist/http/csv-validator.d.ts +23 -0
- package/dist/http/csv-validator.js +83 -0
- package/dist/http/csv-validator.js.map +1 -0
- package/dist/http/output-formatters.d.ts +28 -0
- package/dist/http/output-formatters.js +197 -0
- package/dist/http/output-formatters.js.map +1 -0
- package/dist/http/result-stream.d.ts +35 -0
- package/dist/http/result-stream.js +78 -0
- package/dist/http/result-stream.js.map +1 -0
- package/dist/http/router.d.ts +7 -1
- package/dist/http/router.js +93 -46
- package/dist/http/router.js.map +1 -1
- package/dist/postgres/postgres-store.js +7 -5
- package/dist/postgres/postgres-store.js.map +1 -1
- package/dist/postgres/purge-sql.js +15 -2
- package/dist/postgres/purge-sql.js.map +1 -1
- package/dist/postgres/specification-result-sql.d.ts +2 -0
- package/dist/postgres/specification-result-sql.js +49 -5
- package/dist/postgres/specification-result-sql.js.map +1 -1
- package/dist/postgres/specification-sql.js +28 -6
- package/dist/postgres/specification-sql.js.map +1 -1
- package/docs/design/feeds-persistent-proxy-plan.md +294 -0
- package/integration-test/integration-test/jinaga-test/package-lock.json +6 -0
- package/integration-test/jinaga-test/Dockerfile +1 -6
- package/integration-test/jinaga-test/http-test-helpers.js +77 -0
- package/integration-test/jinaga-test/jinaga.test.js +9 -9
- package/integration-test/jinaga-test/package-lock.json +329 -32
- package/integration-test/jinaga-test/package.json +5 -1
- package/integration-test/jinaga-test/read-csv.test.js +330 -0
- package/integration-test/jinaga-test/read-endpoint.test.js +442 -0
- package/integration-test/jinaga-test/read-performance.test.js +221 -0
- package/integration-test/jinaga-test/read-streaming.test.js +199 -0
- package/integration-test/jinaga-test/specification-conditions.test.js +147 -0
- package/integration-test/jinaga-test/test-helpers.js +12 -0
- package/integration-test/jinaga-test/time-projection.test.js +235 -0
- package/package.json +3 -2
- package/src/authorization/authorization-keystore.ts +4 -4
- package/src/http/csv-metadata.ts +34 -0
- package/src/http/csv-validator.ts +90 -0
- package/src/http/output-formatters.ts +196 -0
- package/src/http/result-stream.ts +69 -0
- package/src/http/router.ts +112 -33
- package/src/postgres/postgres-store.ts +13 -6
- package/src/postgres/purge-sql.ts +18 -2
- package/src/postgres/specification-result-sql.ts +70 -8
- package/src/postgres/specification-sql.ts +36 -8
- package/test/http/csv-validatorSpec.ts +205 -0
- package/test/http/result-streamSpec.ts +151 -0
- package/test/postgres/postgresReadSpec.ts +192 -14
- package/test/postgres/postgresSpecificationSpec.ts +3 -3
- package/test/postgres/specificationConditionsSpec.ts +268 -0
- package/documentation/streaming-feeds-full-results-solution.md +0 -230
|
@@ -35,13 +35,13 @@ function sqlFor(descriptiveString: string, bookmarks: string[] = []) {
|
|
|
35
35
|
},
|
|
36
36
|
emptyRoleMap());
|
|
37
37
|
const start = specification.given.map(input => {
|
|
38
|
-
if (input.type === "Company") {
|
|
38
|
+
if (input.label.type === "Company") {
|
|
39
39
|
return company;
|
|
40
40
|
}
|
|
41
|
-
if (input.type === "Jinaga.User") {
|
|
41
|
+
if (input.label.type === "Jinaga.User") {
|
|
42
42
|
return user;
|
|
43
43
|
}
|
|
44
|
-
throw new Error(`Unknown input type ${input.type}`);
|
|
44
|
+
throw new Error(`Unknown input type ${input.label.type}`);
|
|
45
45
|
});
|
|
46
46
|
const composer = resultSqlFromSpecification(start, specification, factTypes, roleMap, "public");
|
|
47
47
|
if (!composer) {
|
|
@@ -75,8 +75,8 @@ describe("Postgres read", () => {
|
|
|
75
75
|
const sql = composer.getSqlQueries().sqlQuery.sql;
|
|
76
76
|
expect(sql).toEqual(
|
|
77
77
|
`SELECT ` +
|
|
78
|
-
`f1.hash as hash1, f1.fact_id as id1, f1.data as data1, ` +
|
|
79
|
-
`f2.hash as hash2, f2.fact_id as id2, f2.data as data2 ` +
|
|
78
|
+
`f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
|
|
79
|
+
`f2.hash as hash2, f2.fact_id as id2, f2.data as data2, f2.date_learned as timestamp2 ` +
|
|
80
80
|
`FROM public.fact f1 ` +
|
|
81
81
|
`JOIN public.edge e1 ` +
|
|
82
82
|
`ON e1.predecessor_fact_id = f1.fact_id ` +
|
|
@@ -106,8 +106,8 @@ describe("Postgres read", () => {
|
|
|
106
106
|
const sql = tree.sqlQuery.sql;
|
|
107
107
|
expect(sql).toEqual(
|
|
108
108
|
`SELECT ` +
|
|
109
|
-
`f1.hash as hash1, f1.fact_id as id1, f1.data as data1, ` +
|
|
110
|
-
`f3.hash as hash3, f3.fact_id as id3, f3.data as data3 ` +
|
|
109
|
+
`f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
|
|
110
|
+
`f3.hash as hash3, f3.fact_id as id3, f3.data as data3, f3.date_learned as timestamp3 ` +
|
|
111
111
|
`FROM public.fact f1 ` +
|
|
112
112
|
`JOIN public.edge e1 ` +
|
|
113
113
|
`ON e1.predecessor_fact_id = f1.fact_id ` +
|
|
@@ -157,8 +157,8 @@ describe("Postgres read", () => {
|
|
|
157
157
|
const sql = tree.sqlQuery.sql;
|
|
158
158
|
expect(sql).toEqual(
|
|
159
159
|
`SELECT ` +
|
|
160
|
-
`f1.hash as hash1, f1.fact_id as id1, f1.data as data1, ` +
|
|
161
|
-
`f3.hash as hash3, f3.fact_id as id3, f3.data as data3 ` +
|
|
160
|
+
`f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
|
|
161
|
+
`f3.hash as hash3, f3.fact_id as id3, f3.data as data3, f3.date_learned as timestamp3 ` +
|
|
162
162
|
`FROM public.fact f1 ` +
|
|
163
163
|
`JOIN public.edge e1 ` +
|
|
164
164
|
`ON e1.predecessor_fact_id = f1.fact_id ` +
|
|
@@ -213,8 +213,8 @@ describe("Postgres read", () => {
|
|
|
213
213
|
const sql = tree.sqlQuery.sql;
|
|
214
214
|
expect(sql).toEqual(
|
|
215
215
|
`SELECT ` +
|
|
216
|
-
`f1.hash as hash1, f1.fact_id as id1, f1.data as data1, ` +
|
|
217
|
-
`f2.hash as hash2, f2.fact_id as id2, f2.data as data2 ` +
|
|
216
|
+
`f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
|
|
217
|
+
`f2.hash as hash2, f2.fact_id as id2, f2.data as data2, f2.date_learned as timestamp2 ` +
|
|
218
218
|
`FROM public.fact f1 ` +
|
|
219
219
|
`JOIN public.edge e1 ` +
|
|
220
220
|
`ON e1.predecessor_fact_id = f1.fact_id ` +
|
|
@@ -293,9 +293,9 @@ describe("Postgres read", () => {
|
|
|
293
293
|
const queries = composer.getSqlQueries();
|
|
294
294
|
expect(queries.sqlQuery.sql).toEqual(
|
|
295
295
|
`SELECT ` +
|
|
296
|
-
`f1.hash as hash1, f1.fact_id as id1, f1.data as data1, ` +
|
|
297
|
-
`f3.hash as hash3, f3.fact_id as id3, f3.data as data3, ` +
|
|
298
|
-
`f9.hash as hash9, f9.fact_id as id9, f9.data as data9 ` +
|
|
296
|
+
`f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
|
|
297
|
+
`f3.hash as hash3, f3.fact_id as id3, f3.data as data3, f3.date_learned as timestamp3, ` +
|
|
298
|
+
`f9.hash as hash9, f9.fact_id as id9, f9.data as data9, f9.date_learned as timestamp9 ` +
|
|
299
299
|
`FROM public.fact f1 ` +
|
|
300
300
|
`JOIN public.edge e1 ` +
|
|
301
301
|
`ON e1.predecessor_fact_id = f1.fact_id ` +
|
|
@@ -349,4 +349,182 @@ describe("Postgres read", () => {
|
|
|
349
349
|
`) ` +
|
|
350
350
|
`ORDER BY f3.fact_id ASC, f9.fact_id ASC`);
|
|
351
351
|
});
|
|
352
|
+
|
|
353
|
+
it("should read time projection for a successor", () => {
|
|
354
|
+
const { composer, factTypes, roleMap } = sqlFor(`
|
|
355
|
+
(company: Company) {
|
|
356
|
+
department: Department [
|
|
357
|
+
department->company: Company = company
|
|
358
|
+
]
|
|
359
|
+
} => @department
|
|
360
|
+
`);
|
|
361
|
+
|
|
362
|
+
const tree = composer.getSqlQueries();
|
|
363
|
+
const sql = tree.sqlQuery.sql;
|
|
364
|
+
expect(sql).toEqual(
|
|
365
|
+
`SELECT ` +
|
|
366
|
+
`f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
|
|
367
|
+
`f2.hash as hash2, f2.fact_id as id2, f2.data as data2, f2.date_learned as timestamp2 ` +
|
|
368
|
+
`FROM public.fact f1 ` +
|
|
369
|
+
`JOIN public.edge e1 ` +
|
|
370
|
+
`ON e1.predecessor_fact_id = f1.fact_id ` +
|
|
371
|
+
`AND e1.role_id = $3 ` +
|
|
372
|
+
`JOIN public.fact f2 ` +
|
|
373
|
+
`ON f2.fact_id = e1.successor_fact_id ` +
|
|
374
|
+
`WHERE f1.fact_type_id = $1 AND f1.hash = $2 ` +
|
|
375
|
+
`ORDER BY f2.fact_id ASC`
|
|
376
|
+
);
|
|
377
|
+
expect(tree.sqlQuery.parameters).toEqual([
|
|
378
|
+
getFactTypeId(factTypes, 'Company'),
|
|
379
|
+
companyHash,
|
|
380
|
+
roleParameter(roleMap, factTypes, 'Department', 'company')
|
|
381
|
+
]);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("should read time projection with field projection", () => {
|
|
385
|
+
const { composer, factTypes, roleMap } = sqlFor(`
|
|
386
|
+
(company: Company) {
|
|
387
|
+
department: Department [
|
|
388
|
+
department->company: Company = company
|
|
389
|
+
]
|
|
390
|
+
} => {
|
|
391
|
+
name = department.name
|
|
392
|
+
createdAt = @department
|
|
393
|
+
}
|
|
394
|
+
`);
|
|
395
|
+
|
|
396
|
+
const tree = composer.getSqlQueries();
|
|
397
|
+
const sql = tree.sqlQuery.sql;
|
|
398
|
+
expect(sql).toEqual(
|
|
399
|
+
`SELECT ` +
|
|
400
|
+
`f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
|
|
401
|
+
`f2.hash as hash2, f2.fact_id as id2, f2.data as data2, f2.date_learned as timestamp2 ` +
|
|
402
|
+
`FROM public.fact f1 ` +
|
|
403
|
+
`JOIN public.edge e1 ` +
|
|
404
|
+
`ON e1.predecessor_fact_id = f1.fact_id ` +
|
|
405
|
+
`AND e1.role_id = $3 ` +
|
|
406
|
+
`JOIN public.fact f2 ` +
|
|
407
|
+
`ON f2.fact_id = e1.successor_fact_id ` +
|
|
408
|
+
`WHERE f1.fact_type_id = $1 AND f1.hash = $2 ` +
|
|
409
|
+
`ORDER BY f2.fact_id ASC`
|
|
410
|
+
);
|
|
411
|
+
expect(tree.sqlQuery.parameters).toEqual([
|
|
412
|
+
getFactTypeId(factTypes, 'Company'),
|
|
413
|
+
companyHash,
|
|
414
|
+
roleParameter(roleMap, factTypes, 'Department', 'company')
|
|
415
|
+
]);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("should read time projection with fact projection", () => {
|
|
419
|
+
const { composer, factTypes, roleMap } = sqlFor(`
|
|
420
|
+
(company: Company) {
|
|
421
|
+
department: Department [
|
|
422
|
+
department->company: Company = company
|
|
423
|
+
]
|
|
424
|
+
} => {
|
|
425
|
+
department = department
|
|
426
|
+
createdAt = @department
|
|
427
|
+
}
|
|
428
|
+
`);
|
|
429
|
+
|
|
430
|
+
const tree = composer.getSqlQueries();
|
|
431
|
+
const sql = tree.sqlQuery.sql;
|
|
432
|
+
expect(sql).toEqual(
|
|
433
|
+
`SELECT ` +
|
|
434
|
+
`f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
|
|
435
|
+
`f2.hash as hash2, f2.fact_id as id2, f2.data as data2, f2.date_learned as timestamp2 ` +
|
|
436
|
+
`FROM public.fact f1 ` +
|
|
437
|
+
`JOIN public.edge e1 ` +
|
|
438
|
+
`ON e1.predecessor_fact_id = f1.fact_id ` +
|
|
439
|
+
`AND e1.role_id = $3 ` +
|
|
440
|
+
`JOIN public.fact f2 ` +
|
|
441
|
+
`ON f2.fact_id = e1.successor_fact_id ` +
|
|
442
|
+
`WHERE f1.fact_type_id = $1 AND f1.hash = $2 ` +
|
|
443
|
+
`ORDER BY f2.fact_id ASC`
|
|
444
|
+
);
|
|
445
|
+
expect(tree.sqlQuery.parameters).toEqual([
|
|
446
|
+
getFactTypeId(factTypes, 'Company'),
|
|
447
|
+
companyHash,
|
|
448
|
+
roleParameter(roleMap, factTypes, 'Department', 'company')
|
|
449
|
+
]);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("should read time projection with multiple timestamps", () => {
|
|
453
|
+
const { composer, factTypes, roleMap } = sqlFor(`
|
|
454
|
+
(company: Company) {
|
|
455
|
+
department: Department [
|
|
456
|
+
department->company: Company = company
|
|
457
|
+
]
|
|
458
|
+
} => {
|
|
459
|
+
companyCreatedAt = @company
|
|
460
|
+
department = department
|
|
461
|
+
departmentCreatedAt = @department
|
|
462
|
+
}
|
|
463
|
+
`);
|
|
464
|
+
|
|
465
|
+
const tree = composer.getSqlQueries();
|
|
466
|
+
const sql = tree.sqlQuery.sql;
|
|
467
|
+
expect(sql).toEqual(
|
|
468
|
+
`SELECT ` +
|
|
469
|
+
`f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
|
|
470
|
+
`f2.hash as hash2, f2.fact_id as id2, f2.data as data2, f2.date_learned as timestamp2 ` +
|
|
471
|
+
`FROM public.fact f1 ` +
|
|
472
|
+
`JOIN public.edge e1 ` +
|
|
473
|
+
`ON e1.predecessor_fact_id = f1.fact_id ` +
|
|
474
|
+
`AND e1.role_id = $3 ` +
|
|
475
|
+
`JOIN public.fact f2 ` +
|
|
476
|
+
`ON f2.fact_id = e1.successor_fact_id ` +
|
|
477
|
+
`WHERE f1.fact_type_id = $1 AND f1.hash = $2 ` +
|
|
478
|
+
`ORDER BY f2.fact_id ASC`
|
|
479
|
+
);
|
|
480
|
+
expect(tree.sqlQuery.parameters).toEqual([
|
|
481
|
+
getFactTypeId(factTypes, 'Company'),
|
|
482
|
+
companyHash,
|
|
483
|
+
roleParameter(roleMap, factTypes, 'Department', 'company')
|
|
484
|
+
]);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("should read time projection with nested successor", () => {
|
|
488
|
+
const { composer, factTypes, roleMap } = sqlFor(`
|
|
489
|
+
(company: Company) {
|
|
490
|
+
department: Department [
|
|
491
|
+
department->company: Company = company
|
|
492
|
+
]
|
|
493
|
+
project: Project [
|
|
494
|
+
project->department: Department = department
|
|
495
|
+
]
|
|
496
|
+
} => {
|
|
497
|
+
projectName = project.name
|
|
498
|
+
projectCreatedAt = @project
|
|
499
|
+
}
|
|
500
|
+
`);
|
|
501
|
+
|
|
502
|
+
const tree = composer.getSqlQueries();
|
|
503
|
+
const sql = tree.sqlQuery.sql;
|
|
504
|
+
expect(sql).toEqual(
|
|
505
|
+
`SELECT ` +
|
|
506
|
+
`f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
|
|
507
|
+
`f2.hash as hash2, f2.fact_id as id2, f2.data as data2, f2.date_learned as timestamp2, ` +
|
|
508
|
+
`f3.hash as hash3, f3.fact_id as id3, f3.data as data3, f3.date_learned as timestamp3 ` +
|
|
509
|
+
`FROM public.fact f1 ` +
|
|
510
|
+
`JOIN public.edge e1 ` +
|
|
511
|
+
`ON e1.predecessor_fact_id = f1.fact_id ` +
|
|
512
|
+
`AND e1.role_id = $3 ` +
|
|
513
|
+
`JOIN public.fact f2 ` +
|
|
514
|
+
`ON f2.fact_id = e1.successor_fact_id ` +
|
|
515
|
+
`JOIN public.edge e2 ` +
|
|
516
|
+
`ON e2.predecessor_fact_id = f2.fact_id ` +
|
|
517
|
+
`AND e2.role_id = $4 ` +
|
|
518
|
+
`JOIN public.fact f3 ` +
|
|
519
|
+
`ON f3.fact_id = e2.successor_fact_id ` +
|
|
520
|
+
`WHERE f1.fact_type_id = $1 AND f1.hash = $2 ` +
|
|
521
|
+
`ORDER BY f2.fact_id ASC, f3.fact_id ASC`
|
|
522
|
+
);
|
|
523
|
+
expect(tree.sqlQuery.parameters).toEqual([
|
|
524
|
+
getFactTypeId(factTypes, 'Company'),
|
|
525
|
+
companyHash,
|
|
526
|
+
roleParameter(roleMap, factTypes, 'Department', 'company'),
|
|
527
|
+
roleParameter(roleMap, factTypes, 'Project', 'department')
|
|
528
|
+
]);
|
|
529
|
+
});
|
|
352
530
|
});
|
|
@@ -36,13 +36,13 @@ function sqlFor(descriptiveString: string, bookmarks: string[] = []) {
|
|
|
36
36
|
},
|
|
37
37
|
emptyRoleMap());
|
|
38
38
|
const start = specification.given.map(input => {
|
|
39
|
-
if (input.type === 'Root') {
|
|
39
|
+
if (input.label.type === 'Root') {
|
|
40
40
|
return root;
|
|
41
41
|
}
|
|
42
|
-
if (input.type === 'Jinaga.User') {
|
|
42
|
+
if (input.label.type === 'Jinaga.User') {
|
|
43
43
|
return user;
|
|
44
44
|
}
|
|
45
|
-
throw new Error(`Unknown input type ${input.type}`);
|
|
45
|
+
throw new Error(`Unknown input type ${input.label.type}`);
|
|
46
46
|
});
|
|
47
47
|
const sqlQueries = sqlFromSpecification(start, "public", bookmarks, 100, specification, factTypes, roleMap);
|
|
48
48
|
return { sqlQueries, factTypes, roleMap };
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { dehydrateReference, getAllFactTypes, getAllRoles, SpecificationParser } from "jinaga";
|
|
2
|
+
|
|
3
|
+
import { addFactType, addRole, emptyFactTypeMap, emptyRoleMap, getFactTypeId, getRoleId } from "../../src/postgres/maps";
|
|
4
|
+
import { sqlFromSpecification } from "../../src/postgres/specification-sql";
|
|
5
|
+
|
|
6
|
+
const root = dehydrateReference({ type: 'Root' });
|
|
7
|
+
const rootHash = root.hash;
|
|
8
|
+
|
|
9
|
+
function parseSpecification(input: string) {
|
|
10
|
+
const parser = new SpecificationParser(input);
|
|
11
|
+
parser.skipWhitespace();
|
|
12
|
+
return parser.parseSpecification();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function sqlFor(descriptiveString: string, bookmarks: string[] = []) {
|
|
16
|
+
const specification = parseSpecification(descriptiveString);
|
|
17
|
+
const factTypeNames = getAllFactTypes(specification);
|
|
18
|
+
|
|
19
|
+
// Build a fact type map containing all fact types in the specification.
|
|
20
|
+
// Filter out fact types named "Unknown".
|
|
21
|
+
const factTypes = factTypeNames.filter(t => t !== 'Unknown').reduce(
|
|
22
|
+
(f, factType, i) => addFactType(f, factType, i + 1),
|
|
23
|
+
emptyFactTypeMap());
|
|
24
|
+
|
|
25
|
+
// Build a role map containing all roles in the specification.
|
|
26
|
+
// Filter out the roles named "unknown", and those of unknown fact types.
|
|
27
|
+
let roleMap = getAllRoles(specification).filter(r => r.name !== 'unknown').reduce(
|
|
28
|
+
(r, role, i) => {
|
|
29
|
+
const factTypeId = getFactTypeId(factTypes, role.successorType);
|
|
30
|
+
if (!factTypeId) {
|
|
31
|
+
return r;
|
|
32
|
+
}
|
|
33
|
+
return addRole(r, factTypeId, role.name, i + 1);
|
|
34
|
+
},
|
|
35
|
+
emptyRoleMap());
|
|
36
|
+
const start = specification.given.map(input => {
|
|
37
|
+
if (input.label.type === 'Root') {
|
|
38
|
+
return root;
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`Unknown input type ${input.label.type}`);
|
|
41
|
+
});
|
|
42
|
+
const sqlQueries = sqlFromSpecification(start, "public", bookmarks, 100, specification, factTypes, roleMap);
|
|
43
|
+
return { sqlQueries, factTypes, roleMap };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function roleParameter(roleMap: Map<number, Map<string, number>>, factTypes: Map<string, number>, factTypeName: string, roleName: string): number {
|
|
47
|
+
const factTypeId = getFactTypeId(factTypes, factTypeName);
|
|
48
|
+
if (!factTypeId) {
|
|
49
|
+
throw new Error(`Unknown fact type ${factTypeName}`);
|
|
50
|
+
}
|
|
51
|
+
const roleId = getRoleId(roleMap, factTypeId, roleName);
|
|
52
|
+
if (!roleId) {
|
|
53
|
+
throw new Error(`Unknown role ${roleName} in fact type ${factTypeName}`);
|
|
54
|
+
}
|
|
55
|
+
return roleId;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe("Specification conditions", () => {
|
|
59
|
+
describe("Processing given conditions", () => {
|
|
60
|
+
it("should generate SQL for single existential condition on given", () => {
|
|
61
|
+
const { sqlQueries, factTypes, roleMap } = sqlFor(`
|
|
62
|
+
(root: Root [
|
|
63
|
+
E {
|
|
64
|
+
tag: MyApplication.Tag [
|
|
65
|
+
tag->root: Root = root
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
]) {
|
|
69
|
+
project: MyApplication.Project [
|
|
70
|
+
project->root: Root = root
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
`);
|
|
74
|
+
|
|
75
|
+
expect(sqlQueries.length).toBe(1);
|
|
76
|
+
const query = sqlQueries[0];
|
|
77
|
+
|
|
78
|
+
// The SQL should include an EXISTS clause for the condition on the given
|
|
79
|
+
expect(query.sql).toContain('EXISTS');
|
|
80
|
+
expect(query.sql).toEqual(
|
|
81
|
+
'SELECT f2.hash as hash2, ' +
|
|
82
|
+
'sort(array[f2.fact_id], \'desc\') as bookmark ' +
|
|
83
|
+
'FROM public.fact f1 ' +
|
|
84
|
+
'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
|
|
85
|
+
'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
|
|
86
|
+
'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
|
|
87
|
+
'AND EXISTS (' +
|
|
88
|
+
'SELECT 1 ' +
|
|
89
|
+
'FROM public.edge e2 ' +
|
|
90
|
+
'JOIN public.fact f3 ON f3.fact_id = e2.successor_fact_id ' +
|
|
91
|
+
'WHERE e2.predecessor_fact_id = f1.fact_id AND e2.role_id = $4' +
|
|
92
|
+
') ' +
|
|
93
|
+
'AND sort(array[f2.fact_id], \'desc\') > $5 ' +
|
|
94
|
+
'ORDER BY bookmark ASC ' +
|
|
95
|
+
'LIMIT $6'
|
|
96
|
+
);
|
|
97
|
+
expect(query.parameters).toEqual([
|
|
98
|
+
getFactTypeId(factTypes, 'Root'),
|
|
99
|
+
rootHash,
|
|
100
|
+
roleParameter(roleMap, factTypes, 'MyApplication.Project', 'root'),
|
|
101
|
+
roleParameter(roleMap, factTypes, 'MyApplication.Tag', 'root'),
|
|
102
|
+
[],
|
|
103
|
+
100
|
|
104
|
+
]);
|
|
105
|
+
expect(query.labels).toEqual([
|
|
106
|
+
{
|
|
107
|
+
type: 'MyApplication.Project',
|
|
108
|
+
index: 2
|
|
109
|
+
}
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
it("should generate SQL for multiple conditions on same given", () => {
|
|
113
|
+
const { sqlQueries, factTypes, roleMap } = sqlFor(`
|
|
114
|
+
(root: Root [
|
|
115
|
+
E {
|
|
116
|
+
tag: MyApplication.Tag [
|
|
117
|
+
tag->root: Root = root
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
E {
|
|
121
|
+
category: MyApplication.Category [
|
|
122
|
+
category->root: Root = root
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
]) {
|
|
126
|
+
project: MyApplication.Project [
|
|
127
|
+
project->root: Root = root
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
`);
|
|
131
|
+
|
|
132
|
+
expect(sqlQueries.length).toBe(1);
|
|
133
|
+
const query = sqlQueries[0];
|
|
134
|
+
|
|
135
|
+
// The SQL should include TWO EXISTS clauses for the two conditions on the given
|
|
136
|
+
const existsCount = (query.sql.match(/EXISTS/g) || []).length;
|
|
137
|
+
expect(existsCount).toBe(2);
|
|
138
|
+
|
|
139
|
+
// Both EXISTS clauses should be connected with AND
|
|
140
|
+
expect(query.sql).toContain('AND EXISTS');
|
|
141
|
+
|
|
142
|
+
// Verify the complete SQL structure with both EXISTS clauses
|
|
143
|
+
expect(query.sql).toEqual(
|
|
144
|
+
'SELECT f2.hash as hash2, ' +
|
|
145
|
+
'sort(array[f2.fact_id], \'desc\') as bookmark ' +
|
|
146
|
+
'FROM public.fact f1 ' +
|
|
147
|
+
'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
|
|
148
|
+
'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
|
|
149
|
+
'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
|
|
150
|
+
'AND EXISTS (' +
|
|
151
|
+
'SELECT 1 ' +
|
|
152
|
+
'FROM public.edge e2 ' +
|
|
153
|
+
'JOIN public.fact f3 ON f3.fact_id = e2.successor_fact_id ' +
|
|
154
|
+
'WHERE e2.predecessor_fact_id = f1.fact_id AND e2.role_id = $4' +
|
|
155
|
+
') ' +
|
|
156
|
+
'AND EXISTS (' +
|
|
157
|
+
'SELECT 1 ' +
|
|
158
|
+
'FROM public.edge e3 ' +
|
|
159
|
+
'JOIN public.fact f4 ON f4.fact_id = e3.successor_fact_id ' +
|
|
160
|
+
'WHERE e3.predecessor_fact_id = f1.fact_id AND e3.role_id = $5' +
|
|
161
|
+
') ' +
|
|
162
|
+
'AND sort(array[f2.fact_id], \'desc\') > $6 ' +
|
|
163
|
+
'ORDER BY bookmark ASC ' +
|
|
164
|
+
'LIMIT $7'
|
|
165
|
+
);
|
|
166
|
+
expect(query.parameters).toEqual([
|
|
167
|
+
getFactTypeId(factTypes, 'Root'),
|
|
168
|
+
rootHash,
|
|
169
|
+
roleParameter(roleMap, factTypes, 'MyApplication.Project', 'root'),
|
|
170
|
+
roleParameter(roleMap, factTypes, 'MyApplication.Tag', 'root'),
|
|
171
|
+
roleParameter(roleMap, factTypes, 'MyApplication.Category', 'root'),
|
|
172
|
+
[],
|
|
173
|
+
100
|
|
174
|
+
]);
|
|
175
|
+
expect(query.labels).toEqual([
|
|
176
|
+
{
|
|
177
|
+
type: 'MyApplication.Project',
|
|
178
|
+
index: 2
|
|
179
|
+
}
|
|
180
|
+
]);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should generate SQL for conditions on multiple givens", () => {
|
|
184
|
+
const { sqlQueries, factTypes, roleMap } = sqlFor(`
|
|
185
|
+
(root1: Root [
|
|
186
|
+
E {
|
|
187
|
+
tag1: MyApplication.Tag [
|
|
188
|
+
tag1->root: Root = root1
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
], root2: Root [
|
|
192
|
+
E {
|
|
193
|
+
tag2: MyApplication.Tag [
|
|
194
|
+
tag2->root: Root = root2
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
]) {
|
|
198
|
+
project: MyApplication.Project [
|
|
199
|
+
project->root1: Root = root1
|
|
200
|
+
project->root2: Root = root2
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
`);
|
|
204
|
+
|
|
205
|
+
expect(sqlQueries.length).toBe(1);
|
|
206
|
+
const query = sqlQueries[0];
|
|
207
|
+
|
|
208
|
+
// The SQL should include TWO EXISTS clauses, one for each given's condition
|
|
209
|
+
const existsCount = (query.sql.match(/EXISTS/g) || []).length;
|
|
210
|
+
expect(existsCount).toBe(2);
|
|
211
|
+
|
|
212
|
+
// Verify the SQL contains conditions for both givens
|
|
213
|
+
expect(query.sql).toContain('AND EXISTS');
|
|
214
|
+
|
|
215
|
+
// The SQL should reference both f1 (root1) and f3 (root2) in the EXISTS clauses
|
|
216
|
+
expect(query.sql).toMatch(/WHERE e\d+\.predecessor_fact_id = f1\.fact_id/);
|
|
217
|
+
expect(query.sql).toMatch(/WHERE e\d+\.predecessor_fact_id = f3\.fact_id/);
|
|
218
|
+
|
|
219
|
+
// Verify the complete SQL structure
|
|
220
|
+
expect(query.sql).toEqual(
|
|
221
|
+
'SELECT f2.hash as hash2, ' +
|
|
222
|
+
'sort(array[f2.fact_id], \'desc\') as bookmark ' +
|
|
223
|
+
'FROM public.fact f1 ' +
|
|
224
|
+
'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
|
|
225
|
+
'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
|
|
226
|
+
'JOIN public.edge e2 ON e2.successor_fact_id = f2.fact_id AND e2.role_id = $6 ' +
|
|
227
|
+
'JOIN public.fact f3 ON f3.fact_id = e2.predecessor_fact_id ' +
|
|
228
|
+
'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
|
|
229
|
+
'AND f3.fact_type_id = $4 AND f3.hash = $5 ' +
|
|
230
|
+
'AND EXISTS (' +
|
|
231
|
+
'SELECT 1 ' +
|
|
232
|
+
'FROM public.edge e3 ' +
|
|
233
|
+
'JOIN public.fact f4 ON f4.fact_id = e3.successor_fact_id ' +
|
|
234
|
+
'WHERE e3.predecessor_fact_id = f1.fact_id AND e3.role_id = $7' +
|
|
235
|
+
') ' +
|
|
236
|
+
'AND EXISTS (' +
|
|
237
|
+
'SELECT 1 ' +
|
|
238
|
+
'FROM public.edge e4 ' +
|
|
239
|
+
'JOIN public.fact f5 ON f5.fact_id = e4.successor_fact_id ' +
|
|
240
|
+
'WHERE e4.predecessor_fact_id = f3.fact_id AND e4.role_id = $8' +
|
|
241
|
+
') ' +
|
|
242
|
+
'AND sort(array[f2.fact_id], \'desc\') > $9 ' +
|
|
243
|
+
'ORDER BY bookmark ASC ' +
|
|
244
|
+
'LIMIT $10'
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
expect(query.parameters).toEqual([
|
|
248
|
+
getFactTypeId(factTypes, 'Root'),
|
|
249
|
+
rootHash,
|
|
250
|
+
roleParameter(roleMap, factTypes, 'MyApplication.Project', 'root1'),
|
|
251
|
+
getFactTypeId(factTypes, 'Root'),
|
|
252
|
+
rootHash,
|
|
253
|
+
roleParameter(roleMap, factTypes, 'MyApplication.Project', 'root2'),
|
|
254
|
+
roleParameter(roleMap, factTypes, 'MyApplication.Tag', 'root'),
|
|
255
|
+
roleParameter(roleMap, factTypes, 'MyApplication.Tag', 'root'),
|
|
256
|
+
[],
|
|
257
|
+
100
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
expect(query.labels).toEqual([
|
|
261
|
+
{
|
|
262
|
+
type: 'MyApplication.Project',
|
|
263
|
+
index: 2
|
|
264
|
+
}
|
|
265
|
+
]);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|