task(ids): replace remaining Date.now() auto-ids with randomUUID - fixes BUG-1 collision class across 9 entities
This commit is contained in:
@@ -56,7 +56,7 @@ export class SqliteAdapter implements DatabaseInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createUser(data: Partial<DBUser>): DBUser {
|
createUser(data: Partial<DBUser>): DBUser {
|
||||||
const id = data.id ?? `user-${Date.now()}`;
|
const id = data.id ?? `user-${randomUUID().slice(0, 8)}`;
|
||||||
this.db.prepare(
|
this.db.prepare(
|
||||||
'INSERT INTO users (id, email, password_hash, name, role) VALUES (?, ?, ?, ?, ?)',
|
'INSERT INTO users (id, email, password_hash, name, role) VALUES (?, ?, ?, ?, ?)',
|
||||||
).run(id, data.email!, data.password_hash!, data.name!, data.role ?? 'planer');
|
).run(id, data.email!, data.password_hash!, data.name!, data.role ?? 'planer');
|
||||||
@@ -110,7 +110,7 @@ export class SqliteAdapter implements DatabaseInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createProject(data: Partial<DBProject>): DBProject {
|
createProject(data: Partial<DBProject>): DBProject {
|
||||||
const id = data.id ?? `proj-${Date.now()}`;
|
const id = data.id ?? `proj-${randomUUID().slice(0, 8)}`;
|
||||||
this.db.prepare(
|
this.db.prepare(
|
||||||
'INSERT INTO projects (id, name, description, owner_id, folder_id) VALUES (?, ?, ?, ?, ?)',
|
'INSERT INTO projects (id, name, description, owner_id, folder_id) VALUES (?, ?, ?, ?, ?)',
|
||||||
).run(id, data.name ?? 'Unbenannt', data.description ?? null, data.owner_id ?? 'user-default', data.folder_id ?? null);
|
).run(id, data.name ?? 'Unbenannt', data.description ?? null, data.owner_id ?? 'user-default', data.folder_id ?? null);
|
||||||
@@ -149,7 +149,7 @@ export class SqliteAdapter implements DatabaseInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createProjectFolder(data: Partial<DBProjectFolder>): DBProjectFolder {
|
createProjectFolder(data: Partial<DBProjectFolder>): DBProjectFolder {
|
||||||
const id = data.id ?? `pfolder-${Date.now()}`;
|
const id = data.id ?? `pfolder-${randomUUID().slice(0, 8)}`;
|
||||||
this.db.prepare(
|
this.db.prepare(
|
||||||
'INSERT INTO project_folders (id, name, parent_id, owner_id) VALUES (?, ?, ?, ?)',
|
'INSERT INTO project_folders (id, name, parent_id, owner_id) VALUES (?, ?, ?, ?)',
|
||||||
).run(id, data.name ?? 'Neuer Ordner', data.parent_id ?? null, data.owner_id ?? 'user-default');
|
).run(id, data.name ?? 'Neuer Ordner', data.parent_id ?? null, data.owner_id ?? 'user-default');
|
||||||
@@ -183,7 +183,7 @@ export class SqliteAdapter implements DatabaseInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createDrawing(data: Partial<DBDrawing>): DBDrawing {
|
createDrawing(data: Partial<DBDrawing>): DBDrawing {
|
||||||
const id = data.id ?? `draw-${Date.now()}`;
|
const id = data.id ?? `draw-${randomUUID().slice(0, 8)}`;
|
||||||
this.db.prepare(
|
this.db.prepare(
|
||||||
'INSERT INTO drawings (id, project_id, name, data_json) VALUES (?, ?, ?, ?)',
|
'INSERT INTO drawings (id, project_id, name, data_json) VALUES (?, ?, ?, ?)',
|
||||||
).run(id, data.project_id!, data.name ?? 'Unbenannt', data.data_json ?? '{}');
|
).run(id, data.project_id!, data.name ?? 'Unbenannt', data.data_json ?? '{}');
|
||||||
@@ -212,7 +212,7 @@ export class SqliteAdapter implements DatabaseInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createLayer(data: Partial<DBLayer>): DBLayer {
|
createLayer(data: Partial<DBLayer>): DBLayer {
|
||||||
const id = data.id ?? `layer-${Date.now()}`;
|
const id = data.id ?? `layer-${randomUUID().slice(0, 8)}`;
|
||||||
// Idempotent: if a layer with the same id already exists, return it.
|
// Idempotent: if a layer with the same id already exists, return it.
|
||||||
// This prevents race conditions under React StrictMode double-render.
|
// This prevents race conditions under React StrictMode double-render.
|
||||||
const existing = this.db.prepare('SELECT * FROM layers WHERE id = ?').get(id) as DBLayer | undefined;
|
const existing = this.db.prepare('SELECT * FROM layers WHERE id = ?').get(id) as DBLayer | undefined;
|
||||||
@@ -249,7 +249,7 @@ export class SqliteAdapter implements DatabaseInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createElement(data: Partial<DBElement>): DBElement {
|
createElement(data: Partial<DBElement>): DBElement {
|
||||||
const id = data.id ?? `elem-${Date.now()}`;
|
const id = data.id ?? `elem-${randomUUID().slice(0, 8)}`;
|
||||||
this.db.prepare(
|
this.db.prepare(
|
||||||
'INSERT INTO elements (id, drawing_id, layer_id, type, x, y, width, height, properties_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
'INSERT INTO elements (id, drawing_id, layer_id, type, x, y, width, height, properties_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||||
).run(id, data.drawing_id!, data.layer_id!, data.type!, data.x ?? 0, data.y ?? 0,
|
).run(id, data.drawing_id!, data.layer_id!, data.type!, data.x ?? 0, data.y ?? 0,
|
||||||
@@ -281,7 +281,7 @@ export class SqliteAdapter implements DatabaseInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createBlock(data: Partial<DBBlock>): DBBlock {
|
createBlock(data: Partial<DBBlock>): DBBlock {
|
||||||
const id = data.id ?? `block-${Date.now()}`;
|
const id = data.id ?? `block-${randomUUID().slice(0, 8)}`;
|
||||||
this.db.prepare(
|
this.db.prepare(
|
||||||
'INSERT INTO blocks (id, drawing_id, name, description, category, elements_json, thumbnail) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
'INSERT INTO blocks (id, drawing_id, name, description, category, elements_json, thumbnail) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||||
).run(id, data.drawing_id!, data.name ?? 'Block', data.description ?? null,
|
).run(id, data.drawing_id!, data.name ?? 'Block', data.description ?? null,
|
||||||
@@ -352,7 +352,7 @@ export class SqliteAdapter implements DatabaseInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createNotification(data: Partial<DBNotification>): DBNotification {
|
createNotification(data: Partial<DBNotification>): DBNotification {
|
||||||
const id = data.id ?? `notif-${Date.now()}`;
|
const id = data.id ?? `notif-${randomUUID().slice(0, 8)}`;
|
||||||
this.db.prepare(
|
this.db.prepare(
|
||||||
'INSERT INTO notifications (id, user_id, type, title, message, read) VALUES (?, ?, ?, ?, ?, ?)',
|
'INSERT INTO notifications (id, user_id, type, title, message, read) VALUES (?, ?, ?, ?, ?, ?)',
|
||||||
).run(id, data.user_id!, data.type ?? 'info', data.title!, data.message!, data.read ?? 0);
|
).run(id, data.user_id!, data.type ?? 'info', data.title!, data.message!, data.read ?? 0);
|
||||||
@@ -377,7 +377,7 @@ export class SqliteAdapter implements DatabaseInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createProjectShare(data: Partial<DBProjectShare>): DBProjectShare {
|
createProjectShare(data: Partial<DBProjectShare>): DBProjectShare {
|
||||||
const id = data.id ?? `share-${Date.now()}`;
|
const id = data.id ?? `share-${randomUUID().slice(0, 8)}`;
|
||||||
const shareToken = data.share_token ?? randomUUID();
|
const shareToken = data.share_token ?? randomUUID();
|
||||||
this.db.prepare(
|
this.db.prepare(
|
||||||
'INSERT INTO project_shares (id, project_id, shared_with_email, shared_by, permission, share_token) VALUES (?, ?, ?, ?, ?, ?)',
|
'INSERT INTO project_shares (id, project_id, shared_with_email, shared_by, permission, share_token) VALUES (?, ?, ?, ?, ?, ?)',
|
||||||
|
|||||||
@@ -381,3 +381,72 @@ describe('SqliteAdapter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── ID-Kollisionsklasse: Date.now() → randomUUID (BUG-1-Familie) ──
|
||||||
|
// Deterministischer Beweis: Date.now() wird via Fake-Timers EINGEFROREN,
|
||||||
|
// damit zwei Creates garantiert im selben Tick landen. Mit Date.now()-IDs
|
||||||
|
// kollidiert der zweite INSERT am PRIMARY KEY (UNIQUE-Exception); mit
|
||||||
|
// randomUUID sind die IDs stets eindeutig.
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
const FROZEN_NOW = 1756000000000;
|
||||||
|
|
||||||
|
describe('SqliteAdapter – auto-ID uniqueness within same tick', () => {
|
||||||
|
let db: SqliteAdapter;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(FROZEN_NOW);
|
||||||
|
db = new SqliteAdapter(':memory:');
|
||||||
|
await db.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
db.close();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('createUser liefert unterschiedliche IDs bei zwei Creates im selben Tick', () => {
|
||||||
|
const a = db.createUser({ email: `tick-a-${Date.now()}@x.de`, password_hash: 'h', name: 'A' });
|
||||||
|
const b = db.createUser({ email: `tick-b-${Date.now()}@x.de`, password_hash: 'h', name: 'B' });
|
||||||
|
expect(b.id).not.toBe(a.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('createProject/createProjectFolder liefern unterschiedliche IDs im selben Tick', () => {
|
||||||
|
const p1 = db.createProject({ name: 'P1' });
|
||||||
|
const p2 = db.createProject({ name: 'P2' });
|
||||||
|
expect(p2.id).not.toBe(p1.id);
|
||||||
|
const f1 = db.createProjectFolder({ name: 'F1' });
|
||||||
|
const f2 = db.createProjectFolder({ name: 'F2' });
|
||||||
|
expect(f2.id).not.toBe(f1.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('createDrawing/createLayer/createElement/createBlock liefern unterschiedliche IDs im selben Tick', () => {
|
||||||
|
const proj = db.createProject({ name: 'DrawHost' });
|
||||||
|
const d1 = db.createDrawing({ project_id: proj.id, name: 'D1' });
|
||||||
|
const d2 = db.createDrawing({ project_id: proj.id, name: 'D2' });
|
||||||
|
expect(d2.id).not.toBe(d1.id);
|
||||||
|
|
||||||
|
const l1 = db.createLayer({ drawing_id: d1.id, name: 'L1' });
|
||||||
|
const l2 = db.createLayer({ drawing_id: d1.id, name: 'L2' });
|
||||||
|
expect(l2.id).not.toBe(l1.id);
|
||||||
|
|
||||||
|
const e1 = db.createElement({ drawing_id: d1.id, layer_id: l1.id, type: 'line' });
|
||||||
|
const e2 = db.createElement({ drawing_id: d1.id, layer_id: l1.id, type: 'line' });
|
||||||
|
expect(e2.id).not.toBe(e1.id);
|
||||||
|
|
||||||
|
const b1 = db.createBlock({ drawing_id: d1.id, name: 'B1' });
|
||||||
|
const b2 = db.createBlock({ drawing_id: d1.id, name: 'B2' });
|
||||||
|
expect(b2.id).not.toBe(b1.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('createNotification/createProjectShare liefern unterschiedliche IDs im selben Tick', () => {
|
||||||
|
const proj = db.createProject({ name: 'ShareHost' });
|
||||||
|
const n1 = db.createNotification({ user_id: 'user-default', title: 'T1', message: 'M1' });
|
||||||
|
const n2 = db.createNotification({ user_id: 'user-default', title: 'T2', message: 'M2' });
|
||||||
|
expect(n2.id).not.toBe(n1.id);
|
||||||
|
|
||||||
|
const s1 = db.createProjectShare({ project_id: proj.id, shared_with_email: 'a@x.de', shared_by: 'user-default' });
|
||||||
|
const s2 = db.createProjectShare({ project_id: proj.id, shared_with_email: 'b@x.de', shared_by: 'user-default' });
|
||||||
|
expect(s2.id).not.toBe(s1.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user