jinaga-server 3.7.3 → 3.7.4

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.
@@ -23,7 +23,7 @@ jobs:
23
23
  # Steps represent a sequence of tasks that will be executed as part of the job
24
24
  steps:
25
25
  # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
26
- - uses: actions/checkout@v4
26
+ - uses: actions/checkout@v7
27
27
 
28
28
  - name: Build and Test
29
29
  run: |
@@ -11,13 +11,15 @@ jobs:
11
11
  contents: read
12
12
  id-token: write
13
13
  steps:
14
- - uses: actions/checkout@v4
15
- - uses: actions/setup-node@v4
14
+ - uses: actions/checkout@v7
15
+ - uses: actions/setup-node@v7
16
16
  with:
17
- node-version: '20.x'
17
+ node-version: '24.x'
18
18
  registry-url: 'https://registry.npmjs.org'
19
- - run: npm install -g npm@latest
20
- - run: npm install
21
- - run: npm publish
19
+ - run: npm ci
20
+ # Force NODE_AUTH_TOKEN empty so .npmrc has no static token and npm
21
+ # falls through to OIDC trusted publishing. setup-node defaults this
22
+ # to GITHUB_TOKEN, which the npm registry rejects (masked as 404).
23
+ - run: npm publish --provenance
22
24
  env:
23
25
  NODE_AUTH_TOKEN: ''
@@ -14,11 +14,11 @@ jobs:
14
14
  runs-on: ubuntu-latest
15
15
 
16
16
  steps:
17
- - uses: actions/checkout@v4
17
+ - uses: actions/checkout@v7
18
18
 
19
- - uses: actions/setup-node@v4
19
+ - uses: actions/setup-node@v7
20
20
  with:
21
- node-version: '20.x'
21
+ node-version: '24.x'
22
22
 
23
23
  - name: Get versions
24
24
  id: versions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinaga-server",
3
- "version": "3.7.3",
3
+ "version": "3.7.4",
4
4
  "description": "Data management for web and mobile applications.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,115 @@
1
+ import { buildFeeds, dehydrateReference, getAllFactTypes, getAllRoles, Specification, SpecificationParser } from "jinaga";
2
+
3
+ import { addFactType, addRole, emptyFactTypeMap, emptyRoleMap, FactTypeMap, getFactTypeId, RoleMap } from "../../src/postgres/maps";
4
+ import { sqlFromFeed } from "../../src/postgres/specification-sql";
5
+
6
+ // Regression coverage for jinaga-server#179.
7
+ //
8
+ // The issue named two candidate loci for a `Jinaga.User`-given feed returning
9
+ // empty despite matching data: the distribution decision (jinaga.js) and the
10
+ // Postgres feed SQL (this repo, `sqlFromFeed`). These tests pin down the
11
+ // second: the feed SQL generated for a specification whose given is typed
12
+ // `Jinaga.User` is structurally identical to the SQL for the same join shape
13
+ // against any other given type. There is no `Jinaga.User` special-casing in
14
+ // jinaga-server's query generation, so the empty-result trigger does not live
15
+ // here.
16
+
17
+ function parseSpecification(input: string): Specification {
18
+ const parser = new SpecificationParser(input);
19
+ parser.skipWhitespace();
20
+ return parser.parseSpecification();
21
+ }
22
+
23
+ function buildMaps(specification: Specification): { factTypes: FactTypeMap; roleMap: RoleMap } {
24
+ // Assign deterministic type ids in first-seen order so two specifications
25
+ // with the same shape but different type names produce identical parameter
26
+ // sequences. Filter the synthetic "Unknown" fact type and "unknown" role
27
+ // name (matching the other Postgres SQL generator specs) so they cannot
28
+ // shift the id assignment. Roles get ids offset well past the type ids.
29
+ const factTypes = getAllFactTypes(specification)
30
+ .filter(factType => factType !== 'Unknown')
31
+ .reduce((f, factType, i) => addFactType(f, factType, i + 1), emptyFactTypeMap());
32
+ const roleMap = getAllRoles(specification)
33
+ .filter(role => role.name !== 'unknown')
34
+ .reduce((r, role, i) => {
35
+ const factTypeId = getFactTypeId(factTypes, role.successorType);
36
+ if (!factTypeId) {
37
+ return r;
38
+ }
39
+ return addRole(r, factTypeId, role.name, i + 100);
40
+ }, emptyRoleMap());
41
+ return { factTypes, roleMap };
42
+ }
43
+
44
+ // Pure: generates the feed SQL for a single-feed specification. All assertions
45
+ // live in the tests (or beforeAll) so a parse/build failure surfaces as a test
46
+ // failure rather than a suite-collection error.
47
+ function feedSqlFor(descriptiveString: string, startType: string) {
48
+ const specification = parseSpecification(descriptiveString);
49
+ const { factTypes, roleMap } = buildMaps(specification);
50
+ const feeds = buildFeeds(specification);
51
+ const start = [dehydrateReference({ type: startType, key: "value" })];
52
+ const query = sqlFromFeed(feeds[0], start, "public", "", 100, factTypes, roleMap);
53
+ return { query, feedCount: feeds.length, factTypes };
54
+ }
55
+
56
+ // The canonical "owned by me" shape: a successor edged to the given user
57
+ // through a role named `owner`.
58
+ const USER_GIVEN_SPEC = `
59
+ (user: Jinaga.User) {
60
+ activity: Networking.Activity [
61
+ activity->owner: Jinaga.User = user
62
+ ]
63
+ }`;
64
+
65
+ // The identical join shape rooted at a non-user given.
66
+ const ACTIVITY_GIVEN_SPEC = `
67
+ (activity: Networking.Activity) {
68
+ log: Networking.ActivityCrmLog [
69
+ log->activity: Networking.Activity = activity
70
+ ]
71
+ }`;
72
+
73
+ describe("Jinaga.User-given feed SQL (issue #179)", () => {
74
+ let userGiven: ReturnType<typeof feedSqlFor>;
75
+ let activityGiven: ReturnType<typeof feedSqlFor>;
76
+
77
+ beforeAll(() => {
78
+ userGiven = feedSqlFor(USER_GIVEN_SPEC, "Jinaga.User");
79
+ activityGiven = feedSqlFor(ACTIVITY_GIVEN_SPEC, "Networking.Activity");
80
+ // Both shapes must reduce to a single feed for the comparison to be
81
+ // apples-to-apples.
82
+ expect(userGiven.feedCount).toBe(1);
83
+ expect(activityGiven.feedCount).toBe(1);
84
+ });
85
+
86
+ it("produces a satisfiable, single feed query for a Jinaga.User given", () => {
87
+ // A satisfiable query is the whole point: a null query here would be
88
+ // the silent-empty the issue describes. The given's type is loaded and
89
+ // the predecessor edge on `owner` is generated.
90
+ expect(userGiven.query).not.toBeNull();
91
+ expect(userGiven.query!.sql).toContain("f1.fact_type_id = $1 AND f1.hash = $2");
92
+ expect(userGiven.query!.sql).toContain("public.edge");
93
+ expect(userGiven.query!.labels.length).toBe(1);
94
+ });
95
+
96
+ it("generates SQL structurally identical to a non-user given of the same shape", () => {
97
+ // No Jinaga.User special-casing: the two SQL strings match exactly.
98
+ expect(userGiven.query).not.toBeNull();
99
+ expect(activityGiven.query).not.toBeNull();
100
+ expect(userGiven.query!.sql).toEqual(activityGiven.query!.sql);
101
+ });
102
+
103
+ it("binds the given type id (from the fact-type map) and hash as the leading parameters", () => {
104
+ // The given fact type participates in the WHERE clause like any other —
105
+ // it is not dropped or defaulted to a constant for Jinaga.User. Derive
106
+ // the expected id from the map built for this spec so the assertion is
107
+ // not coupled to getAllFactTypes ordering.
108
+ const userReference = dehydrateReference({ type: "Jinaga.User", key: "value" });
109
+ const expectedTypeId = getFactTypeId(userGiven.factTypes, "Jinaga.User");
110
+ expect(expectedTypeId).toBeDefined();
111
+ expect(userGiven.query).not.toBeNull();
112
+ expect(userGiven.query!.parameters[0]).toBe(expectedTypeId);
113
+ expect(userGiven.query!.parameters[1]).toBe(userReference.hash);
114
+ });
115
+ });