Skip to content

Commit 4ea2b83

Browse files
committed
Handle method now has unified way to pass expected version
1 parent 41699c5 commit 4ea2b83

8 files changed

Lines changed: 125 additions & 105 deletions

File tree

src/packages/pongo/src/core/collection/handle.ts

Lines changed: 55 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -49,58 +49,75 @@ export type DocumentCommandHandlerOptions<T extends PongoDocument> = {
4949
};
5050
};
5151

52+
export type DocumentCommandHandlerInput = {
53+
_id: string;
54+
expectedVersion?: ExpectedDocumentVersion;
55+
};
56+
57+
type DocumentChange<T extends PongoDocument> =
58+
| {
59+
type: 'noop';
60+
existing: WithIdAndVersion<T> | null;
61+
versionMismatch?: boolean;
62+
}
63+
| { type: 'insert'; doc: WithId<T> }
64+
| {
65+
type: 'replace';
66+
existing: WithIdAndVersion<T>;
67+
result: WithId<T>;
68+
_version?: bigint;
69+
}
70+
| { type: 'delete'; docId: string; _version?: bigint };
71+
72+
type DocumentHandlerResult = { succeeded: boolean; newVersion?: bigint };
73+
5274
export function DocumentCommandHandler<T extends PongoDocument>(
5375
deps: DocumentCommandHandlerOptions<T>,
5476
): {
5577
(
56-
id: string,
78+
id: string | DocumentCommandHandlerInput,
5779
handler: DocumentHandler<T>,
58-
options?: HandleOptions & BatchHandleOptions,
80+
options?: HandleOptions,
5981
): Promise<PongoHandleResult<T>>;
6082
(
61-
ids: string[],
83+
ids: string[] | DocumentCommandHandlerInput[],
6284
handler: DocumentHandler<T>,
6385
options?: HandleOptions & BatchHandleOptions,
6486
): Promise<PongoHandleResult<T>[]>;
6587
} {
6688
const fn = async (
67-
id: string | string[],
89+
input:
90+
| string
91+
| string[]
92+
| DocumentCommandHandlerInput
93+
| DocumentCommandHandlerInput[],
6894
handler: DocumentHandler<T>,
69-
options?: HandleOptions & BatchHandleOptions,
95+
options?: HandleOptions | BatchHandleOptions,
7096
): Promise<PongoHandleResult<T> | PongoHandleResult<T>[]> => {
71-
if (Array.isArray(id)) {
72-
return handleDocuments(deps, id, handler, options);
73-
}
74-
const { expectedVersion, ...batchOptions } = options ?? {};
75-
const input: DocumentInput[] = expectedVersion
76-
? [{ _id: id, expectedVersion }]
77-
: [id];
78-
const [result] = await handleDocuments(deps, input, handler, batchOptions);
79-
return result!;
97+
const result = await handleDocuments(
98+
deps,
99+
normalizeInput(input),
100+
handler,
101+
options,
102+
);
103+
return Array.isArray(input) ? result : result[0]!;
80104
};
81105
return fn as ReturnType<typeof DocumentCommandHandler<T>>;
82106
}
83107

84-
type DocumentInput =
85-
| string
86-
| { _id: string; expectedVersion?: ExpectedDocumentVersion };
108+
function normalizeInput(
109+
input:
110+
| string
111+
| string[]
112+
| DocumentCommandHandlerInput
113+
| DocumentCommandHandlerInput[],
114+
): DocumentCommandHandlerInput[] {
115+
if (typeof input === 'string') return [{ _id: input }];
87116

88-
type DocumentChange<T extends PongoDocument> =
89-
| {
90-
type: 'noop';
91-
existing: WithIdAndVersion<T> | null;
92-
versionMismatch?: boolean;
93-
}
94-
| { type: 'insert'; doc: WithId<T> }
95-
| {
96-
type: 'replace';
97-
existing: WithIdAndVersion<T>;
98-
result: WithId<T>;
99-
_version?: bigint;
100-
}
101-
| { type: 'delete'; docId: string; _version?: bigint };
117+
if (!Array.isArray(input)) return [input];
102118

103-
type DocumentHandlerResult = { succeeded: boolean; newVersion?: bigint };
119+
return input.map((item) => (typeof item === 'string' ? { _id: item } : item));
120+
}
104121

105122
function hasVersionMismatch<T extends PongoDocument>(
106123
existing: WithIdAndVersion<T> | null,
@@ -119,7 +136,6 @@ function toDocumentChange<T extends PongoDocument>(
119136
docId: string,
120137
existing: WithIdAndVersion<T> | null,
121138
result: T | null,
122-
skipConcurrencyCheck?: boolean,
123139
): DocumentChange<T> {
124140
if (deepEquals(existing as T | null, result))
125141
return { type: 'noop', existing };
@@ -134,14 +150,14 @@ function toDocumentChange<T extends PongoDocument>(
134150
return {
135151
type: 'delete',
136152
docId,
137-
...(!skipConcurrencyCheck && { _version: existing._version }),
153+
_version: existing._version,
138154
};
139155

140156
return {
141157
type: 'replace',
142158
existing: existing!,
143159
result: { ...result, _id: docId } as WithId<T>,
144-
...(!skipConcurrencyCheck && { _version: existing!._version }),
160+
_version: existing!._version,
145161
};
146162
}
147163

@@ -272,26 +288,25 @@ async function handleDocument<T extends PongoDocument>(
272288
existing: WithIdAndVersion<T> | null,
273289
handler: DocumentHandler<T>,
274290
expectedVersion: ExpectedDocumentVersion | undefined,
275-
skipConcurrencyCheck?: boolean,
276291
): Promise<DocumentChange<T>> {
277292
if (hasVersionMismatch(existing, expectedVersion))
278293
return { type: 'noop', existing, versionMismatch: true };
279294

280295
const result = await handler(existing ? ({ ...existing } as T) : null);
281296

282-
return toDocumentChange(id, existing, result, skipConcurrencyCheck);
297+
return toDocumentChange(id, existing, result);
283298
}
284299

285300
async function handleDocuments<T extends PongoDocument>(
286301
deps: DocumentCommandHandlerOptions<T>,
287-
inputs: DocumentInput[],
302+
inputs: DocumentCommandHandlerInput[],
288303
handler: DocumentHandler<T>,
289304
options?: BatchHandleOptions,
290305
): Promise<PongoHandleResult<T>[]> {
291306
if (inputs.length === 0) return [];
292307

293308
const { storage } = deps;
294-
const { skipConcurrencyCheck, parallel, ...operationOptions } = options ?? {};
309+
const { parallel, ...operationOptions } = options ?? {};
295310
const items = inputs.map((input) =>
296311
typeof input === 'string' ? { _id: input } : input,
297312
);
@@ -310,13 +325,7 @@ async function handleDocuments<T extends PongoDocument>(
310325
const changes = await mapAsync(
311326
itemsWithDocs,
312327
({ _id, existing, expectedVersion }) =>
313-
handleDocument(
314-
_id,
315-
existing,
316-
handler,
317-
expectedVersion,
318-
skipConcurrencyCheck,
319-
),
328+
handleDocument(_id, existing, handler, expectedVersion),
320329
{ parallel },
321330
);
322331

src/packages/pongo/src/core/collection/handle.unit.spec.ts

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,10 @@ describe('handle — single document version checking', () => {
187187
const deps = makeDeps({ fetchByIds: vi.fn().mockResolvedValue([null]) });
188188
const handle = DocumentCommandHandler(deps);
189189

190-
const result = await handle('id-1', () => ({ name: 'X' }), {
191-
expectedVersion: 'DOCUMENT_EXISTS',
192-
});
190+
const result = await handle(
191+
{ _id: 'id-1', expectedVersion: 'DOCUMENT_EXISTS' },
192+
() => ({ name: 'X' }),
193+
);
193194

194195
expect(deps.storage.insertMany).not.toHaveBeenCalled();
195196
expect(result.successful).toBe(false);
@@ -199,9 +200,9 @@ describe('handle — single document version checking', () => {
199200
const deps = makeDeps({ fetchByIds: vi.fn().mockResolvedValue([null]) });
200201
const handle = DocumentCommandHandler(deps);
201202

202-
const result = await handle('id-1', () => ({ name: 'X' }), {
203-
expectedVersion: 1n,
204-
});
203+
const result = await handle({ _id: 'id-1', expectedVersion: 1n }, () => ({
204+
name: 'X',
205+
}));
205206

206207
expect(deps.storage.insertMany).not.toHaveBeenCalled();
207208
expect(result.successful).toBe(false);
@@ -213,9 +214,10 @@ describe('handle — single document version checking', () => {
213214
});
214215
const handle = DocumentCommandHandler(deps);
215216

216-
const result = await handle('id-1', (d) => ({ ...d!, name: 'X' }), {
217-
expectedVersion: 'DOCUMENT_DOES_NOT_EXIST',
218-
});
217+
const result = await handle(
218+
{ _id: 'id-1', expectedVersion: 'DOCUMENT_DOES_NOT_EXIST' },
219+
(d) => ({ ...d!, name: 'X' }),
220+
);
219221

220222
expect(deps.storage.replaceMany).not.toHaveBeenCalled();
221223
expect(result.successful).toBe(false);
@@ -227,9 +229,10 @@ describe('handle — single document version checking', () => {
227229
});
228230
const handle = DocumentCommandHandler(deps);
229231

230-
const result = await handle('id-1', (d) => ({ ...d!, name: 'X' }), {
231-
expectedVersion: 333n,
232-
});
232+
const result = await handle(
233+
{ _id: 'id-1', expectedVersion: 333n },
234+
(d) => ({ ...d!, name: 'X' }),
235+
);
233236

234237
expect(deps.storage.replaceMany).not.toHaveBeenCalled();
235238
expect(result.successful).toBe(false);
@@ -242,7 +245,10 @@ describe('handle — single document version checking', () => {
242245
});
243246
const handle = DocumentCommandHandler(deps);
244247

245-
const result = await handle('id-1', (d) => d, { expectedVersion: 99n });
248+
const result = await handle(
249+
{ _id: 'id-1', expectedVersion: 99n },
250+
(d) => d,
251+
);
246252

247253
expect(deps.storage.replaceMany).not.toHaveBeenCalled();
248254
expect(result.successful).toBe(false);
@@ -255,9 +261,10 @@ describe('handle — single document version checking', () => {
255261
});
256262
const handle = DocumentCommandHandler(deps);
257263

258-
const result = await handle('id-1', (d) => ({ ...d!, name: 'Bob' }), {
259-
expectedVersion: 5n,
260-
});
264+
const result = await handle({ _id: 'id-1', expectedVersion: 5n }, (d) => ({
265+
...d!,
266+
name: 'Bob',
267+
}));
261268

262269
expect(deps.storage.replaceMany).toHaveBeenCalledWith(
263270
[expect.objectContaining({ _id: 'id-1', _version: 5n })],
@@ -273,7 +280,10 @@ describe('handle — single document version checking', () => {
273280
});
274281
const handle = DocumentCommandHandler(deps);
275282

276-
const result = await handle('id-1', () => null, { expectedVersion: 5n });
283+
const result = await handle(
284+
{ _id: 'id-1', expectedVersion: 5n },
285+
() => null,
286+
);
277287

278288
expect(deps.storage.deleteManyByIds).toHaveBeenCalledWith(
279289
[expect.objectContaining({ _id: 'id-1', _version: 5n })],
@@ -438,19 +448,17 @@ describe('handle — batch concurrency', () => {
438448
expect(result[0]!.successful).toBe(false);
439449
});
440450

441-
it('skips version check in storage when skipConcurrencyCheck is true', async () => {
451+
it('skips version check in storage when no version is provided', async () => {
442452
const deps = makeDeps({
443453
fetchByIds: vi.fn().mockResolvedValue([doc('id-1', 'Alice', 5n)]),
444454
replaceMany: vi.fn().mockResolvedValue(replaceResult(['id-1'])),
445455
});
446456
const handle = DocumentCommandHandler(deps);
447457

448-
await handle(['id-1'], (d) => ({ ...d!, name: 'Forced' }), {
449-
skipConcurrencyCheck: true,
450-
});
458+
await handle([{ _id: 'id-1' }], (d) => ({ ...d!, name: 'Forced' }));
451459

452460
expect(deps.storage.replaceMany).toHaveBeenCalledWith(
453-
[{ _id: 'id-1', name: 'Forced' }],
461+
[{ _id: 'id-1', name: 'Forced', _version: 5n }],
454462
expect.anything(),
455463
);
456464
});

src/packages/pongo/src/core/typing/operations.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import type {
1717
import { v7 as uuid } from 'uuid';
1818
import type { MaybePromise } from '.';
1919
import type { CacheConfig, PongoCache, PongoTransactionCache } from '../cache';
20-
import type { PongoCollectionSchemaComponent } from '../collection';
20+
import type {
21+
DocumentCommandHandlerInput,
22+
PongoCollectionSchemaComponent,
23+
} from '../collection';
2124
import type { PongoDatabaseSchemaComponent } from '../database/pongoDatabaseSchemaComponent';
2225
import type { AnyPongoDriver, ExtractPongoDriverOptions } from '../drivers';
2326
import { ConcurrencyError } from '../errors';
@@ -188,12 +191,9 @@ export type UpdateManyOptions = {
188191
>;
189192
} & CollectionOperationOptions;
190193

191-
export type HandleOptions = {
192-
expectedVersion?: ExpectedDocumentVersion;
193-
} & CollectionOperationOptions;
194+
export type HandleOptions = CollectionOperationOptions;
194195

195196
export type BatchHandleOptions = {
196-
skipConcurrencyCheck?: boolean;
197197
parallel?: boolean;
198198
} & CollectionOperationOptions;
199199

@@ -286,12 +286,12 @@ export interface PongoCollection<T extends PongoDocument> {
286286
options?: CollectionOperationOptions,
287287
): Promise<PongoCollection<T>>;
288288
handle(
289-
id: string,
289+
id: string | DocumentCommandHandlerInput,
290290
handle: DocumentHandler<T>,
291291
options?: HandleOptions,
292292
): Promise<PongoHandleResult<T>>;
293293
handle(
294-
id: string[],
294+
id: string[] | DocumentCommandHandlerInput[],
295295
handle: DocumentHandler<T>,
296296
options?: BatchHandleOptions,
297297
): Promise<PongoHandleResult<T>[]>;

src/packages/pongo/src/e2e/postgresql/pg/compatibilityTest.e2e.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import {
1010
import assert from 'assert';
1111
import type { Db as MongoDb, ObjectId } from 'mongodb';
1212
import { MongoClient as OriginalMongoClient } from 'mongodb';
13-
import { afterAll, beforeAll, describe, it } from 'vitest';
1413
import { v7 as uuid } from 'uuid';
14+
import { afterAll, beforeAll, describe, it } from 'vitest';
1515
import { pgDriver, usePgPongoDriver } from '../../../pg';
1616
import { MongoClient, type Db } from '../../../shim';
1717

@@ -1073,6 +1073,7 @@ describe('MongoDB Compatibility Tests', () => {
10731073
assert(resultPongo.successful);
10741074
assert.deepStrictEqual(resultPongo.document, {
10751075
...updatedDoc,
1076+
_id: pongoInsertResult.insertedId,
10761077
_version: 2n,
10771078
});
10781079

src/packages/pongo/src/e2e/postgresql/pg/postgres.e2e.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {
66
} from '@testcontainers/postgresql';
77
import assert from 'assert';
88
import console from 'console';
9-
import { afterAll, beforeAll, describe, it } from 'vitest';
109
import { v7 as uuid } from 'uuid';
10+
import { afterAll, beforeAll, describe, it } from 'vitest';
1111
import {
1212
pongoClient,
1313
type ObjectId,
@@ -1136,6 +1136,7 @@ describe('MongoDB Compatibility Tests', () => {
11361136
assert(resultPongo.successful);
11371137
assert.deepStrictEqual(resultPongo.document, {
11381138
...updatedDoc,
1139+
_id: pongoInsertResult.insertedId!,
11391140
_version: 2n,
11401141
});
11411142

0 commit comments

Comments
 (0)