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.
- package/.github/workflows/main.yml +1 -1
- package/.github/workflows/publish.yml +8 -6
- package/.github/workflows/upgrade-jinaga.yml +3 -3
- package/dist/postgres/postgres-keystore.js +13 -2
- package/dist/postgres/postgres-keystore.js.map +1 -1
- package/integration-test/jinaga-test/postgres-keystore-race.test.js +68 -0
- package/package.json +1 -1
- package/src/postgres/postgres-keystore.ts +14 -2
- package/test/postgres/userGivenFeedSqlSpec.ts +115 -0
|
@@ -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@
|
|
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@
|
|
15
|
-
- uses: actions/setup-node@
|
|
14
|
+
- uses: actions/checkout@v7
|
|
15
|
+
- uses: actions/setup-node@v7
|
|
16
16
|
with:
|
|
17
|
-
node-version: '
|
|
17
|
+
node-version: '24.x'
|
|
18
18
|
registry-url: 'https://registry.npmjs.org'
|
|
19
|
-
- run: npm
|
|
20
|
-
|
|
21
|
-
-
|
|
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@
|
|
17
|
+
- uses: actions/checkout@v7
|
|
18
18
|
|
|
19
|
-
- uses: actions/setup-node@
|
|
19
|
+
- uses: actions/setup-node@v7
|
|
20
20
|
with:
|
|
21
|
-
node-version: '
|
|
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)
|
|
123
|
-
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
});
|