jinaga-server 3.7.1 → 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
@@ -119,8 +119,19 @@ class PostgresKeystore {
119
119
  generateKeyPair(connection, userIdentity) {
120
120
  return __awaiter(this, void 0, void 0, function* () {
121
121
  const keyPair = (0, jinaga_1.generateKeyPair)();
122
- yield connection.query(`INSERT INTO ${this.schema}.user (provider, user_identifier, private_key, public_key) VALUES ($1, $2, $3, $4)`, [userIdentity.provider, userIdentity.id, keyPair.privatePem, keyPair.publicPem]);
123
- return keyPair;
122
+ const { rows } = yield connection.query(`INSERT INTO ${this.schema}.user (provider, user_identifier, private_key, public_key)
123
+ VALUES ($1, $2, $3, $4)
124
+ ON CONFLICT (user_identifier, provider) DO NOTHING
125
+ RETURNING 1`, [userIdentity.provider, userIdentity.id, keyPair.privatePem, keyPair.publicPem]);
126
+ if (rows.length === 1) {
127
+ return keyPair;
128
+ }
129
+ // Lost the race: a concurrent request inserted this user's key pair
130
+ // between our SELECT and INSERT. The ON CONFLICT clause suppressed the
131
+ // unique-violation (23505) on ux_user, so read back the winner's key
132
+ // pair instead of surfacing a duplicate-key error. selectOrInsertKeyPair
133
+ // will now find exactly one row.
134
+ return this.selectOrInsertKeyPair(connection, userIdentity);
124
135
  });
125
136
  }
126
137
  }
@@ -1 +1 @@
1
- {"version":3,"file":"postgres-keystore.js","sourceRoot":"","sources":["../../src/postgres/postgres-keystore.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"postgres-keystore.js","sourceRoot":"","sources":["../../src/postgres/postgres-keystore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ const { Pool } = require('pg');
2
+ const { PostgresKeystore } = require('jinaga-server');
3
+
4
+ const host = "db";
5
+ const connectionString = `postgresql://dev:devpw@${host}:5432/integrationtest`;
6
+
7
+ const pool = new Pool({
8
+ connectionString
9
+ });
10
+
11
+ describe('PostgresKeystore concurrency race', () => {
12
+ afterAll(async () => {
13
+ await pool.end();
14
+ });
15
+
16
+ beforeEach(async () => {
17
+ // Ensure no row exists yet for the test identities so the
18
+ // select-then-insert path is exercised from a clean state.
19
+ await pool.query(`DELETE FROM public."user" WHERE provider = $1`, ['test-provider']);
20
+ });
21
+
22
+ it('should not fail when two concurrent requests create the same user key pair', async () => {
23
+ const userIdentity = { provider: 'test-provider', id: 'race-user-1' };
24
+
25
+ // Two separate keystore instances model two concurrent requests that
26
+ // each build a fresh runtime (empty in-process cache), as described in
27
+ // issue #177. Both SELECT zero rows and race to INSERT.
28
+ const keystoreA = new PostgresKeystore(pool, 'public');
29
+ const keystoreB = new PostgresKeystore(pool, 'public');
30
+
31
+ const [factA, factB] = await Promise.all([
32
+ keystoreA.getOrCreateUserFact(userIdentity),
33
+ keystoreB.getOrCreateUserFact(userIdentity)
34
+ ]);
35
+
36
+ // Both requests succeed and observe the same public key (the winner's).
37
+ expect(factA.type).toBe('Jinaga.User');
38
+ expect(factB.type).toBe('Jinaga.User');
39
+ expect(factA.fields.publicKey).toBe(factB.fields.publicKey);
40
+ expect(factA.hash).toBe(factB.hash);
41
+
42
+ // Exactly one row was persisted for the identity.
43
+ const { rows } = await pool.query(
44
+ `SELECT public_key FROM public."user" WHERE provider = $1 AND user_identifier = $2`,
45
+ [userIdentity.provider, userIdentity.id]);
46
+ expect(rows.length).toBe(1);
47
+ expect(rows[0].public_key).toBe(factA.fields.publicKey);
48
+ });
49
+
50
+ it('should tolerate many concurrent creations of the same identity', async () => {
51
+ const userIdentity = { provider: 'test-provider', id: 'race-user-2' };
52
+
53
+ // A larger fan-out of fresh runtimes all racing on the same identity.
54
+ const keystores = Array.from({ length: 8 }, () => new PostgresKeystore(pool, 'public'));
55
+
56
+ const facts = await Promise.all(
57
+ keystores.map(keystore => keystore.getOrCreateUserFact(userIdentity)));
58
+
59
+ // Every caller resolves to the same key pair.
60
+ const publicKeys = new Set(facts.map(fact => fact.fields.publicKey));
61
+ expect(publicKeys.size).toBe(1);
62
+
63
+ const { rows } = await pool.query(
64
+ `SELECT public_key FROM public."user" WHERE provider = $1 AND user_identifier = $2`,
65
+ [userIdentity.provider, userIdentity.id]);
66
+ expect(rows.length).toBe(1);
67
+ });
68
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinaga-server",
3
- "version": "3.7.1",
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",
@@ -114,9 +114,21 @@ export class PostgresKeystore implements Keystore {
114
114
 
115
115
  private async generateKeyPair(connection: PoolClient, userIdentity: UserIdentity): Promise<KeyPair> {
116
116
  const keyPair = generateKeyPair();
117
- await connection.query(`INSERT INTO ${this.schema}.user (provider, user_identifier, private_key, public_key) VALUES ($1, $2, $3, $4)`,
117
+ const { rows } = await connection.query(
118
+ `INSERT INTO ${this.schema}.user (provider, user_identifier, private_key, public_key)
119
+ VALUES ($1, $2, $3, $4)
120
+ ON CONFLICT (user_identifier, provider) DO NOTHING
121
+ RETURNING 1`,
118
122
  [userIdentity.provider, userIdentity.id, keyPair.privatePem, keyPair.publicPem]);
119
- return keyPair;
123
+ if (rows.length === 1) {
124
+ return keyPair;
125
+ }
126
+ // Lost the race: a concurrent request inserted this user's key pair
127
+ // between our SELECT and INSERT. The ON CONFLICT clause suppressed the
128
+ // unique-violation (23505) on ux_user, so read back the winner's key
129
+ // pair instead of surfacing a duplicate-key error. selectOrInsertKeyPair
130
+ // will now find exactly one row.
131
+ return this.selectOrInsertKeyPair(connection, userIdentity);
120
132
  }
121
133
  }
122
134
 
@@ -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
+ });