@byline/db-postgres 3.17.1 → 3.19.0
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/database/schema/auth.d.ts +5 -5
- package/dist/database/schema/index.d.ts +455 -455
- package/dist/modules/counters/counters-commands.d.ts +1 -0
- package/dist/modules/counters/counters-commands.js +19 -0
- package/dist/modules/counters/tests/counters-scoped.test.d.ts +8 -0
- package/dist/modules/counters/tests/counters-scoped.test.js +100 -0
- package/dist/modules/storage/storage-commands.d.ts +11 -11
- package/dist/modules/storage/storage-queries.d.ts +12 -12
- package/package.json +12 -12
|
@@ -17,6 +17,7 @@ export declare class CounterCommands implements ICounterCommands {
|
|
|
17
17
|
sequenceName: string;
|
|
18
18
|
}>;
|
|
19
19
|
nextCounterValue(groupName: string): Promise<number>;
|
|
20
|
+
nextScopedCounterValue(scopeName: string): Promise<number>;
|
|
20
21
|
}
|
|
21
22
|
export declare function createCounterCommands(db: DatabaseConnection): ICounterCommands;
|
|
22
23
|
export {};
|
|
@@ -90,6 +90,25 @@ export class CounterCommands {
|
|
|
90
90
|
}
|
|
91
91
|
return typeof row.value === 'number' ? row.value : Number(row.value);
|
|
92
92
|
}
|
|
93
|
+
async nextScopedCounterValue(scopeName) {
|
|
94
|
+
if (!scopeName || typeof scopeName !== 'string') {
|
|
95
|
+
throw new Error(`nextScopedCounterValue: scopeName must be a non-empty string`);
|
|
96
|
+
}
|
|
97
|
+
// Fast path: the scope has been used before — its registry row and
|
|
98
|
+
// sequence already exist, so this is exactly a nextCounterValue call.
|
|
99
|
+
const rows = await this.db
|
|
100
|
+
.select({ sequence_name: counterGroups.sequence_name })
|
|
101
|
+
.from(counterGroups)
|
|
102
|
+
.where(eq(counterGroups.group_name, scopeName))
|
|
103
|
+
.limit(1);
|
|
104
|
+
if (rows.length === 0) {
|
|
105
|
+
// First allocation for this scope: self-register (idempotent — a
|
|
106
|
+
// concurrent racer leaves exactly one sequence + registry row, and
|
|
107
|
+
// both callers draw distinct values from the same sequence).
|
|
108
|
+
await this.ensureCounterGroup(scopeName);
|
|
109
|
+
}
|
|
110
|
+
return this.nextCounterValue(scopeName);
|
|
111
|
+
}
|
|
93
112
|
}
|
|
94
113
|
export function createCounterCommands(db) {
|
|
95
114
|
return new CounterCommands(db);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import { eq, sql } from 'drizzle-orm';
|
|
9
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
10
|
+
import { counterGroups } from '../../../database/schema/index.js';
|
|
11
|
+
import { setupTestDB, teardownTestDB } from '../../../lib/test-helper.js';
|
|
12
|
+
import { createCounterCommands } from '../counters-commands.js';
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Track-and-clean fixtures
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
//
|
|
17
|
+
// Integration tests share the dev database. Each scope this suite touches
|
|
18
|
+
// creates a registry row + a Postgres sequence; track the scope names and
|
|
19
|
+
// drop both on teardown so repeated runs start from a clean slate (and so
|
|
20
|
+
// monotonicity assertions are not satisfied by a previous run's sequence).
|
|
21
|
+
let db;
|
|
22
|
+
let counters;
|
|
23
|
+
const trackedScopes = new Set();
|
|
24
|
+
async function cleanupScope(scopeName) {
|
|
25
|
+
const rows = await db
|
|
26
|
+
.select({ sequence_name: counterGroups.sequence_name })
|
|
27
|
+
.from(counterGroups)
|
|
28
|
+
.where(eq(counterGroups.group_name, scopeName))
|
|
29
|
+
.limit(1);
|
|
30
|
+
const sequenceName = rows[0]?.sequence_name;
|
|
31
|
+
await db.delete(counterGroups).where(eq(counterGroups.group_name, scopeName));
|
|
32
|
+
if (sequenceName) {
|
|
33
|
+
await db.execute(sql.raw(`DROP SEQUENCE IF EXISTS "${sequenceName}"`));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function scope(name) {
|
|
37
|
+
trackedScopes.add(name);
|
|
38
|
+
return name;
|
|
39
|
+
}
|
|
40
|
+
beforeAll(async () => {
|
|
41
|
+
const setup = setupTestDB();
|
|
42
|
+
db = setup.db;
|
|
43
|
+
counters = createCounterCommands(db);
|
|
44
|
+
// Clean any leftovers from a previously aborted run.
|
|
45
|
+
for (const name of trackedScopes) {
|
|
46
|
+
await cleanupScope(name);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
afterAll(async () => {
|
|
50
|
+
for (const name of trackedScopes) {
|
|
51
|
+
await cleanupScope(name);
|
|
52
|
+
}
|
|
53
|
+
await teardownTestDB();
|
|
54
|
+
});
|
|
55
|
+
describe('nextScopedCounterValue', () => {
|
|
56
|
+
it('self-registers an unknown scope and allocates from 1', async () => {
|
|
57
|
+
const scopeName = scope('test:doc-a:files');
|
|
58
|
+
const first = await counters.nextScopedCounterValue(scopeName);
|
|
59
|
+
expect(first).toBe(1);
|
|
60
|
+
// The registry row and its backing sequence now exist.
|
|
61
|
+
const rows = await db
|
|
62
|
+
.select({ sequence_name: counterGroups.sequence_name })
|
|
63
|
+
.from(counterGroups)
|
|
64
|
+
.where(eq(counterGroups.group_name, scopeName));
|
|
65
|
+
expect(rows).toHaveLength(1);
|
|
66
|
+
});
|
|
67
|
+
it('is monotonic within a scope and never reuses a value', async () => {
|
|
68
|
+
const scopeName = scope('test:doc-b:files');
|
|
69
|
+
const a = await counters.nextScopedCounterValue(scopeName);
|
|
70
|
+
const b = await counters.nextScopedCounterValue(scopeName);
|
|
71
|
+
const c = await counters.nextScopedCounterValue(scopeName);
|
|
72
|
+
expect([a, b, c]).toEqual([1, 2, 3]);
|
|
73
|
+
});
|
|
74
|
+
it('scopes are independent — one scope does not advance another', async () => {
|
|
75
|
+
const scopeA = scope('test:doc-c:files');
|
|
76
|
+
const scopeB = scope('test:doc-d:files');
|
|
77
|
+
await counters.nextScopedCounterValue(scopeA);
|
|
78
|
+
await counters.nextScopedCounterValue(scopeA);
|
|
79
|
+
const bFirst = await counters.nextScopedCounterValue(scopeB);
|
|
80
|
+
expect(bFirst).toBe(1);
|
|
81
|
+
});
|
|
82
|
+
it('interoperates with nextCounterValue once registered (same sequence)', async () => {
|
|
83
|
+
const scopeName = scope('test:doc-e:files');
|
|
84
|
+
const viaScoped = await counters.nextScopedCounterValue(scopeName);
|
|
85
|
+
// After self-registration the static-path allocator sees the group too.
|
|
86
|
+
const viaStatic = await counters.nextCounterValue(scopeName);
|
|
87
|
+
expect(viaStatic).toBe(viaScoped + 1);
|
|
88
|
+
});
|
|
89
|
+
it('rejects an empty scope name', async () => {
|
|
90
|
+
await expect(counters.nextScopedCounterValue('')).rejects.toThrow(/scopeName must be a non-empty string/);
|
|
91
|
+
});
|
|
92
|
+
it('parallel allocations within one scope yield distinct values', async () => {
|
|
93
|
+
const scopeName = scope('test:doc-f:files');
|
|
94
|
+
const values = await Promise.all(Array.from({ length: 8 }, () => counters.nextScopedCounterValue(scopeName)));
|
|
95
|
+
const unique = new Set(values);
|
|
96
|
+
expect(unique.size).toBe(8);
|
|
97
|
+
expect(Math.min(...values)).toBe(1);
|
|
98
|
+
expect(Math.max(...values)).toBe(8);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -47,15 +47,15 @@ export declare class CollectionCommands implements ICollectionCommands {
|
|
|
47
47
|
version?: number;
|
|
48
48
|
schemaHash?: string;
|
|
49
49
|
}): Promise<{
|
|
50
|
+
config: unknown;
|
|
50
51
|
created_at: Date;
|
|
51
|
-
updated_at: Date;
|
|
52
52
|
id: string;
|
|
53
|
-
config: unknown;
|
|
54
53
|
path: string;
|
|
55
|
-
singular: string;
|
|
56
54
|
plural: string;
|
|
57
|
-
version: number;
|
|
58
55
|
schema_hash: string | null;
|
|
56
|
+
singular: string;
|
|
57
|
+
updated_at: Date;
|
|
58
|
+
version: number;
|
|
59
59
|
}[]>;
|
|
60
60
|
update(id: string, patch: {
|
|
61
61
|
config?: CollectionDefinition;
|
|
@@ -126,18 +126,18 @@ export declare class DocumentCommands implements IDocumentCommands {
|
|
|
126
126
|
orderKey?: string;
|
|
127
127
|
}): Promise<{
|
|
128
128
|
document: {
|
|
129
|
-
|
|
130
|
-
updated_at: Date;
|
|
131
|
-
id: string;
|
|
129
|
+
change_summary: string | null;
|
|
132
130
|
collection_id: string;
|
|
133
|
-
document_id: string;
|
|
134
131
|
collection_version: number;
|
|
132
|
+
created_at: Date;
|
|
133
|
+
created_by: string | null;
|
|
135
134
|
doc: unknown;
|
|
135
|
+
document_id: string;
|
|
136
136
|
event_type: string;
|
|
137
|
-
|
|
137
|
+
id: string;
|
|
138
138
|
is_deleted: boolean | null;
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
status: string | null;
|
|
140
|
+
updated_at: Date;
|
|
141
141
|
};
|
|
142
142
|
fieldCount: number;
|
|
143
143
|
}>;
|
|
@@ -27,26 +27,26 @@ export declare class CollectionQueries implements ICollectionQueries {
|
|
|
27
27
|
schema_hash: string | null;
|
|
28
28
|
}[]>;
|
|
29
29
|
getCollectionByPath(path: string): Promise<{
|
|
30
|
+
config: unknown;
|
|
30
31
|
created_at: Date;
|
|
31
|
-
updated_at: Date;
|
|
32
32
|
id: string;
|
|
33
|
-
config: unknown;
|
|
34
33
|
path: string;
|
|
35
|
-
singular: string;
|
|
36
34
|
plural: string;
|
|
37
|
-
version: number;
|
|
38
35
|
schema_hash: string | null;
|
|
36
|
+
singular: string;
|
|
37
|
+
updated_at: Date;
|
|
38
|
+
version: number;
|
|
39
39
|
} | undefined>;
|
|
40
40
|
getCollectionById(id: string): Promise<{
|
|
41
|
+
config: unknown;
|
|
41
42
|
created_at: Date;
|
|
42
|
-
updated_at: Date;
|
|
43
43
|
id: string;
|
|
44
|
-
config: unknown;
|
|
45
44
|
path: string;
|
|
46
|
-
singular: string;
|
|
47
45
|
plural: string;
|
|
48
|
-
version: number;
|
|
49
46
|
schema_hash: string | null;
|
|
47
|
+
singular: string;
|
|
48
|
+
updated_at: Date;
|
|
49
|
+
version: number;
|
|
50
50
|
} | undefined>;
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
@@ -250,7 +250,6 @@ export declare class DocumentQueries implements IDocumentQueries {
|
|
|
250
250
|
lenient?: boolean;
|
|
251
251
|
onMissingLocale?: MissingLocalePolicy;
|
|
252
252
|
}): Promise<{
|
|
253
|
-
restoreWarnings?: string[] | undefined;
|
|
254
253
|
document_version_id: string;
|
|
255
254
|
document_id: string;
|
|
256
255
|
path: string;
|
|
@@ -264,6 +263,7 @@ export declare class DocumentQueries implements IDocumentQueries {
|
|
|
264
263
|
availableLocales: string[];
|
|
265
264
|
_availableVersionLocales: string[];
|
|
266
265
|
_localeAgnostic: boolean;
|
|
266
|
+
restoreWarnings?: string[] | undefined;
|
|
267
267
|
} | {
|
|
268
268
|
document_version_id: string;
|
|
269
269
|
document_id: string;
|
|
@@ -299,6 +299,9 @@ export declare class DocumentQueries implements IDocumentQueries {
|
|
|
299
299
|
_availableVersionLocales: string[];
|
|
300
300
|
_localeAgnostic: boolean;
|
|
301
301
|
} | {
|
|
302
|
+
availableLocales?: undefined;
|
|
303
|
+
_availableVersionLocales?: undefined;
|
|
304
|
+
_localeAgnostic?: undefined;
|
|
302
305
|
document_version_id: string;
|
|
303
306
|
document_id: string;
|
|
304
307
|
path: string;
|
|
@@ -309,9 +312,6 @@ export declare class DocumentQueries implements IDocumentQueries {
|
|
|
309
312
|
updated_at: Date;
|
|
310
313
|
created_by: string | null;
|
|
311
314
|
fields: FlattenedStore[];
|
|
312
|
-
availableLocales?: undefined;
|
|
313
|
-
_availableVersionLocales?: undefined;
|
|
314
|
-
_localeAgnostic?: undefined;
|
|
315
315
|
} | null>;
|
|
316
316
|
/**
|
|
317
317
|
* getDocumentByVersion — fetches a specific version and reconstructs its fields.
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/db-postgres",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.19.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -54,24 +54,24 @@
|
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"drizzle-orm": "^0.45.2",
|
|
56
56
|
"npm-run-all": "^4.1.5",
|
|
57
|
-
"pg": "^8.
|
|
58
|
-
"uuid": "^14.0.
|
|
59
|
-
"@byline/admin": "3.
|
|
60
|
-
"@byline/
|
|
61
|
-
"@byline/
|
|
57
|
+
"pg": "^8.22.0",
|
|
58
|
+
"uuid": "^14.0.1",
|
|
59
|
+
"@byline/admin": "3.19.0",
|
|
60
|
+
"@byline/auth": "3.19.0",
|
|
61
|
+
"@byline/core": "3.19.0"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@biomejs/biome": "2.
|
|
65
|
-
"@types/node": "^
|
|
64
|
+
"@biomejs/biome": "2.5.2",
|
|
65
|
+
"@types/node": "^26.1.0",
|
|
66
66
|
"@types/pg": "^8.20.0",
|
|
67
67
|
"chokidar": "^5.0.0",
|
|
68
68
|
"chokidar-cli": "^3.0.0",
|
|
69
69
|
"dotenv": "^17.4.2",
|
|
70
70
|
"drizzle-kit": "^0.31.10",
|
|
71
|
-
"tsc-alias": "^1.
|
|
72
|
-
"tsx": "^4.
|
|
73
|
-
"typescript": "
|
|
74
|
-
"vitest": "^4.1.
|
|
71
|
+
"tsc-alias": "^1.9.0",
|
|
72
|
+
"tsx": "^4.23.0",
|
|
73
|
+
"typescript": "^7.0.2",
|
|
74
|
+
"vitest": "^4.1.10"
|
|
75
75
|
},
|
|
76
76
|
"publishConfig": {
|
|
77
77
|
"access": "public",
|