-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathengine-autonumber-defer.test.ts
More file actions
154 lines (137 loc) · 5.51 KB
/
Copy pathengine-autonumber-defer.test.ts
File metadata and controls
154 lines (137 loc) · 5.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ObjectQL } from './engine';
import { SchemaRegistry } from './registry';
import type { IDataDriver } from '@objectstack/spec/contracts';
/**
* #1603 — autonumber generation is owned by ONE layer.
*
* When the driver advertises `supports.autonumber === true` (the SQL driver,
* which has a persistent `_objectstack_sequences` table), the engine must NOT
* pre-fill `autonumber` fields with its in-memory counter — it defers to the
* driver so the persistent sequence is the single source of truth. When the
* driver does NOT advertise it (memory / mongodb), the engine keeps its
* in-memory fallback so nothing regresses.
*
* A `required` autonumber must pass insert-validation in BOTH cases (the value
* is runtime-owned, assigned after validation in the native-driver case).
*/
vi.mock('./registry', () => {
const instance: any = {
getObject: vi.fn(),
resolveObject: vi.fn((n: string) => instance.getObject(n)),
registerObject: vi.fn(),
getObjectOwner: vi.fn(),
registerNamespace: vi.fn(),
registerKind: vi.fn(),
registerItem: vi.fn(),
registerApp: vi.fn(),
installPackage: vi.fn(),
reset: vi.fn(),
metadata: { get: vi.fn(() => new Map()) },
};
function SchemaRegistry() {
return instance;
}
Object.assign(SchemaRegistry, instance);
return {
SchemaRegistry,
computeFQN: (_ns: string | undefined, name: string) => name,
parseFQN: (fqn: string) => ({ namespace: undefined, shortName: fqn }),
RESERVED_NAMESPACES: new Set(['base', 'system']),
};
});
function makeDriver(supportsAutonumber: boolean): IDataDriver & { created: any[] } {
const created: any[] = [];
const driver: any = {
name: supportsAutonumber ? 'sql' : 'memory',
supports: supportsAutonumber ? { autonumber: true } : {},
connect: vi.fn().mockResolvedValue(undefined),
disconnect: vi.fn().mockResolvedValue(undefined),
// seedAutonumber (fallback path) reads existing rows; none exist yet.
find: vi.fn().mockResolvedValue([]),
findOne: vi.fn(),
create: vi.fn(async (_obj: string, row: any) => {
created.push(row);
// Native driver assigns the autonumber itself (post-validation).
return supportsAutonumber ? { id: 'r1', ...row, doc_no: 'D-0001' } : { id: 'r1', ...row };
}),
update: vi.fn(),
delete: vi.fn(),
count: vi.fn(),
};
driver.created = created;
return driver as any;
}
const DOC_SCHEMA = {
name: 'doc',
fields: {
title: { type: 'text' },
doc_no: { type: 'autonumber', required: true, format: 'D-{0000}' },
},
};
describe('ObjectQL autonumber ownership (#1603)', () => {
let engine: ObjectQL;
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(SchemaRegistry.getObject).mockReturnValue(DOC_SCHEMA as any);
engine = new ObjectQL();
});
it('defers to a native-autonumber driver (does NOT pre-fill) and passes required-validation', async () => {
const driver = makeDriver(true);
engine.registerDriver(driver, true);
await engine.init();
const result = await engine.insert('doc', { title: 'Spec' });
// Engine did NOT generate the value — the row handed to the driver has no doc_no.
expect(driver.created).toHaveLength(1);
expect(driver.created[0].doc_no).toBeUndefined();
// Engine never scanned for a max via the fallback seed path.
expect(driver.find).not.toHaveBeenCalled();
// The driver's value comes back to the caller.
expect(result.doc_no).toBe('D-0001');
});
it('falls back to engine generation for a driver without native autonumber', async () => {
const driver = makeDriver(false);
engine.registerDriver(driver, true);
await engine.init();
const result = await engine.insert('doc', { title: 'Spec' });
// Engine pre-filled the value before calling the driver.
expect(driver.created).toHaveLength(1);
expect(driver.created[0].doc_no).toBe('D-0001');
expect(result.doc_no).toBe('D-0001');
});
// The fallback path renders the SAME format tokens as the SQL driver
// (shared @objectstack/spec renderer), so {field}/{date} grouping must match.
it('fallback renders {field} tokens and counts independently per scope', async () => {
const TASK_SCHEMA = {
name: 'task',
fields: {
zone: { type: 'text' },
task_no: { type: 'autonumber', format: '{zone}{000}' },
},
};
vi.mocked(SchemaRegistry.getObject).mockReturnValue(TASK_SCHEMA as any);
const driver = makeDriver(false);
engine.registerDriver(driver, true);
await engine.init();
const a1 = await engine.insert('task', { zone: 'A' });
const b1 = await engine.insert('task', { zone: 'B' });
const a2 = await engine.insert('task', { zone: 'A' });
expect(a1.task_no).toBe('A001');
expect(b1.task_no).toBe('B001'); // a different scope restarts at 001
expect(a2.task_no).toBe('A002');
});
it('fallback renders {YYYYMMDD} date tokens in the business timezone', async () => {
const AUDIT_SCHEMA = {
name: 'audit',
fields: { audit_no: { type: 'autonumber', format: 'AD{YYYYMMDD}{0000}' } },
};
vi.mocked(SchemaRegistry.getObject).mockReturnValue(AUDIT_SCHEMA as any);
const driver = makeDriver(false);
engine.registerDriver(driver, true);
await engine.init();
const r = await engine.insert('audit', {}, { timezone: 'UTC' } as any);
// Today's UTC day + a fresh per-day counter.
expect(r.audit_no).toMatch(/^AD\d{8}0001$/);
});
});