@tstdl/base 0.93.145 → 0.93.147
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/authentication/tests/authentication.client-service.test.js +15 -19
- package/authentication/tests/authentication.service.test.js +92 -119
- package/notification/tests/notification-client.test.js +39 -50
- package/notification/tests/notification-flow.test.js +204 -238
- package/notification/tests/notification-sse.service.test.js +20 -27
- package/notification/tests/notification-type.service.test.js +17 -20
- package/orm/repository.types.d.ts +13 -2
- package/orm/server/repository.d.ts +60 -4
- package/orm/server/repository.js +126 -25
- package/orm/tests/query-complex.test.js +80 -111
- package/orm/tests/repository-advanced.test.js +100 -143
- package/orm/tests/repository-attributes.test.js +30 -39
- package/orm/tests/repository-compound-primary-key.test.js +67 -75
- package/orm/tests/repository-comprehensive.test.js +76 -101
- package/orm/tests/repository-coverage.test.d.ts +1 -0
- package/orm/tests/repository-coverage.test.js +88 -149
- package/orm/tests/repository-cti-extensive.test.d.ts +1 -0
- package/orm/tests/repository-cti-extensive.test.js +118 -147
- package/orm/tests/repository-cti-mapping.test.d.ts +1 -0
- package/orm/tests/repository-cti-mapping.test.js +29 -42
- package/orm/tests/repository-cti-soft-delete.test.d.ts +1 -0
- package/orm/tests/repository-cti-soft-delete.test.js +25 -37
- package/orm/tests/repository-cti-transactions.test.js +19 -33
- package/orm/tests/repository-cti-upsert-many.test.d.ts +1 -0
- package/orm/tests/repository-cti-upsert-many.test.js +38 -50
- package/orm/tests/repository-cti.test.d.ts +1 -0
- package/orm/tests/repository-cti.test.js +195 -247
- package/orm/tests/repository-expiration.test.d.ts +1 -0
- package/orm/tests/repository-expiration.test.js +46 -59
- package/orm/tests/repository-extra-coverage.test.d.ts +1 -0
- package/orm/tests/repository-extra-coverage.test.js +195 -337
- package/orm/tests/repository-mapping.test.d.ts +1 -0
- package/orm/tests/repository-mapping.test.js +20 -20
- package/orm/tests/repository-regression.test.js +124 -163
- package/orm/tests/repository-search.test.js +30 -44
- package/orm/tests/repository-soft-delete.test.js +54 -79
- package/orm/tests/repository-types.test.js +77 -111
- package/orm/tests/repository-undelete.test.d.ts +2 -0
- package/orm/tests/repository-undelete.test.js +201 -0
- package/package.json +3 -3
- package/task-queue/tests/worker.test.js +5 -5
- package/testing/README.md +38 -16
- package/testing/integration-setup.d.ts +11 -0
- package/testing/integration-setup.js +57 -30
|
@@ -7,17 +7,17 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
8
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
9
|
};
|
|
10
|
-
import { Injector, runInInjectionContext } from '../../injector/index.js';
|
|
11
|
-
import { StringProperty } from '../../schema/index.js';
|
|
12
10
|
import { sql } from 'drizzle-orm';
|
|
13
11
|
import { beforeAll, describe, expect, test } from 'vitest';
|
|
12
|
+
import { StringProperty } from '../../schema/index.js';
|
|
13
|
+
import { setupIntegrationTest } from '../../testing/index.js';
|
|
14
14
|
import { Table } from '../decorators.js';
|
|
15
15
|
import { Entity } from '../entity.js';
|
|
16
|
-
import {
|
|
17
|
-
import { injectRepository } from '../server/repository.js';
|
|
16
|
+
import { getRepository } from '../server/index.js';
|
|
18
17
|
describe('ORM Repository Soft Delete (Integration)', () => {
|
|
19
18
|
let injector;
|
|
20
19
|
let db;
|
|
20
|
+
let repository;
|
|
21
21
|
const schema = 'test_orm_soft_delete';
|
|
22
22
|
let SoftDeleteEntity = class SoftDeleteEntity extends Entity {
|
|
23
23
|
name;
|
|
@@ -30,14 +30,10 @@ describe('ORM Repository Soft Delete (Integration)', () => {
|
|
|
30
30
|
Table('soft_delete_entities', { schema })
|
|
31
31
|
], SoftDeleteEntity);
|
|
32
32
|
beforeAll(async () => {
|
|
33
|
-
injector =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
host: '127.0.0.1', port: 5432, user: 'tstdl', password: 'wf7rq6glrk5jykne', database: 'tstdl',
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
db = injector.resolve(Database);
|
|
33
|
+
({ injector, database: db } = await setupIntegrationTest({
|
|
34
|
+
orm: { schema },
|
|
35
|
+
}));
|
|
36
|
+
repository = injector.resolve(getRepository(SoftDeleteEntity));
|
|
41
37
|
await db.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
|
|
42
38
|
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
|
|
43
39
|
await db.execute(sql `
|
|
@@ -54,90 +50,69 @@ describe('ORM Repository Soft Delete (Integration)', () => {
|
|
|
54
50
|
});
|
|
55
51
|
test('should soft delete an entity by ID', async () => {
|
|
56
52
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
|
|
57
|
-
await
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
expect(allWithDeleted).toHaveLength(1);
|
|
68
|
-
expect(allWithDeleted[0].id).toBe(e1.id);
|
|
69
|
-
});
|
|
53
|
+
const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
|
|
54
|
+
const deleted = await repository.delete(e1.id);
|
|
55
|
+
expect(deleted.id).toBe(e1.id);
|
|
56
|
+
expect(deleted.metadata.deleteTimestamp).toBeDefined();
|
|
57
|
+
expect(await repository.has(e1.id)).toBe(false);
|
|
58
|
+
expect(await repository.tryLoad(e1.id)).toBeUndefined();
|
|
59
|
+
expect(await repository.count()).toBe(0);
|
|
60
|
+
const allWithDeleted = await repository.loadAll({ withDeleted: true });
|
|
61
|
+
expect(allWithDeleted).toHaveLength(1);
|
|
62
|
+
expect(allWithDeleted[0].id).toBe(e1.id);
|
|
70
63
|
});
|
|
71
64
|
test('should soft delete multiple entities by IDs', async () => {
|
|
72
65
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
|
|
73
|
-
await
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
expect(await repository.count()).toBe(0);
|
|
80
|
-
expect(await repository.countByQuery({}, { withDeleted: true })).toBe(2);
|
|
81
|
-
});
|
|
66
|
+
const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
|
|
67
|
+
const e2 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E2' }));
|
|
68
|
+
const deleted = await repository.deleteMany([e1.id, e2.id]);
|
|
69
|
+
expect(deleted).toHaveLength(2);
|
|
70
|
+
expect(await repository.count()).toBe(0);
|
|
71
|
+
expect(await repository.countByQuery({}, { withDeleted: true })).toBe(2);
|
|
82
72
|
});
|
|
83
73
|
test('should soft delete entities by query', async () => {
|
|
84
74
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
|
|
85
|
-
await
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
expect(await repository.count()).toBe(1);
|
|
95
|
-
expect(await repository.countByQuery({}, { withDeleted: true })).toBe(3);
|
|
96
|
-
});
|
|
75
|
+
await repository.insertMany([
|
|
76
|
+
Object.assign(new SoftDeleteEntity(), { name: 'A' }),
|
|
77
|
+
Object.assign(new SoftDeleteEntity(), { name: 'B' }),
|
|
78
|
+
Object.assign(new SoftDeleteEntity(), { name: 'A' }),
|
|
79
|
+
]);
|
|
80
|
+
const deleted = await repository.deleteManyByQuery({ name: 'A' });
|
|
81
|
+
expect(deleted).toHaveLength(2);
|
|
82
|
+
expect(await repository.count()).toBe(1);
|
|
83
|
+
expect(await repository.countByQuery({}, { withDeleted: true })).toBe(3);
|
|
97
84
|
});
|
|
98
85
|
test('should not update soft-deleted entities by default', async () => {
|
|
99
86
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
|
|
100
|
-
await
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const loaded = await repository.tryLoad(e1.id, { withDeleted: true });
|
|
107
|
-
expect(loaded.name).toBe('E1');
|
|
108
|
-
});
|
|
87
|
+
const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
|
|
88
|
+
await repository.delete(e1.id);
|
|
89
|
+
const updated = await repository.tryUpdate(e1.id, { name: 'Updated' });
|
|
90
|
+
expect(updated).toBeUndefined();
|
|
91
|
+
const loaded = await repository.tryLoad(e1.id, { withDeleted: true });
|
|
92
|
+
expect(loaded.name).toBe('E1');
|
|
109
93
|
});
|
|
110
94
|
test('should support hard delete on soft-deleted entities', async () => {
|
|
111
95
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
|
|
112
|
-
await
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
expect(hardDeleted.id).toBe(e1.id);
|
|
118
|
-
expect(await repository.countByQuery({}, { withDeleted: true })).toBe(0);
|
|
119
|
-
});
|
|
96
|
+
const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
|
|
97
|
+
await repository.delete(e1.id);
|
|
98
|
+
const hardDeleted = await repository.hardDelete(e1.id);
|
|
99
|
+
expect(hardDeleted.id).toBe(e1.id);
|
|
100
|
+
expect(await repository.countByQuery({}, { withDeleted: true })).toBe(0);
|
|
120
101
|
});
|
|
121
102
|
test('should ignore already soft-deleted entities when deleting again', async () => {
|
|
122
103
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
|
|
123
|
-
await
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const secondDelete = await repository.tryDelete(e1.id);
|
|
128
|
-
expect(secondDelete).toBeUndefined();
|
|
129
|
-
});
|
|
104
|
+
const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
|
|
105
|
+
await repository.delete(e1.id);
|
|
106
|
+
const secondDelete = await repository.tryDelete(e1.id);
|
|
107
|
+
expect(secondDelete).toBeUndefined();
|
|
130
108
|
});
|
|
131
109
|
test('should hard delete multiple entities by query (including soft-deleted)', async () => {
|
|
132
110
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
|
|
133
|
-
await
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
expect(hardDeleted).toHaveLength(2); // Should hard delete both
|
|
140
|
-
expect(await repository.countByQuery({}, { withDeleted: true })).toBe(0);
|
|
141
|
-
});
|
|
111
|
+
const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
|
|
112
|
+
const e2 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E2' }));
|
|
113
|
+
await repository.delete(e1.id); // Soft delete E1
|
|
114
|
+
const hardDeleted = await repository.hardDeleteManyByQuery({});
|
|
115
|
+
expect(hardDeleted).toHaveLength(2); // Should hard delete both
|
|
116
|
+
expect(await repository.countByQuery({}, { withDeleted: true })).toBe(0);
|
|
142
117
|
});
|
|
143
118
|
});
|
|
@@ -9,16 +9,16 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
};
|
|
10
10
|
import { sql } from 'drizzle-orm';
|
|
11
11
|
import { beforeAll, describe, expect, test } from 'vitest';
|
|
12
|
-
import { Injector, runInInjectionContext } from '../../injector/index.js';
|
|
13
12
|
import { Array, BooleanProperty, Integer, NumberProperty, string, StringProperty } from '../../schema/index.js';
|
|
13
|
+
import { setupIntegrationTest } from '../../testing/index.js';
|
|
14
14
|
import { Table } from '../decorators.js';
|
|
15
15
|
import { Entity } from '../entity.js';
|
|
16
16
|
import { JsonProperty, NumericDateProperty, TimestampProperty } from '../schemas/index.js';
|
|
17
|
-
import {
|
|
18
|
-
import { injectRepository } from '../server/repository.js';
|
|
17
|
+
import { getRepository } from '../server/index.js';
|
|
19
18
|
describe('ORM Repository Types (Integration)', () => {
|
|
20
19
|
let injector;
|
|
21
20
|
let db;
|
|
21
|
+
let repo;
|
|
22
22
|
const schema = 'test_orm_types';
|
|
23
23
|
let TypeEntity = class TypeEntity extends Entity {
|
|
24
24
|
text;
|
|
@@ -66,14 +66,10 @@ describe('ORM Repository Types (Integration)', () => {
|
|
|
66
66
|
Table('type_entities', { schema })
|
|
67
67
|
], TypeEntity);
|
|
68
68
|
beforeAll(async () => {
|
|
69
|
-
injector =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
host: '127.0.0.1', port: 5432, user: 'tstdl', password: 'wf7rq6glrk5jykne', database: 'tstdl',
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
db = injector.resolve(Database);
|
|
69
|
+
({ injector, database: db } = await setupIntegrationTest({
|
|
70
|
+
orm: { schema },
|
|
71
|
+
}));
|
|
72
|
+
repo = injector.resolve(getRepository(TypeEntity));
|
|
77
73
|
await db.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
|
|
78
74
|
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('type_entities')} CASCADE`);
|
|
79
75
|
await db.execute(sql `
|
|
@@ -96,123 +92,93 @@ describe('ORM Repository Types (Integration)', () => {
|
|
|
96
92
|
`);
|
|
97
93
|
});
|
|
98
94
|
test('should persist and retrieve boolean values (true)', async () => {
|
|
99
|
-
await
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const loaded = await repo.load(entity.id);
|
|
105
|
-
expect(loaded.bool).toBe(true);
|
|
106
|
-
});
|
|
95
|
+
const entity = await repo.insert(Object.assign(new TypeEntity(), {
|
|
96
|
+
text: 't', int: 1, float: 1.1, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
|
|
97
|
+
}));
|
|
98
|
+
const loaded = await repo.load(entity.id);
|
|
99
|
+
expect(loaded.bool).toBe(true);
|
|
107
100
|
});
|
|
108
101
|
test('should persist and retrieve boolean values (false)', async () => {
|
|
109
|
-
await
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const loaded = await repo.load(entity.id);
|
|
115
|
-
expect(loaded.bool).toBe(false);
|
|
116
|
-
});
|
|
102
|
+
const entity = await repo.insert(Object.assign(new TypeEntity(), {
|
|
103
|
+
text: 'f', int: 1, float: 1.1, bool: false, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
|
|
104
|
+
}));
|
|
105
|
+
const loaded = await repo.load(entity.id);
|
|
106
|
+
expect(loaded.bool).toBe(false);
|
|
117
107
|
});
|
|
118
108
|
test('should persist and retrieve integer boundaries', async () => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const l2 = await repo.load(e2.id);
|
|
132
|
-
expect(l2.int).toBe(minInt);
|
|
133
|
-
});
|
|
109
|
+
const maxInt = 2147483647;
|
|
110
|
+
const minInt = -2147483648;
|
|
111
|
+
const e1 = await repo.insert(Object.assign(new TypeEntity(), {
|
|
112
|
+
text: 'max', int: maxInt, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
|
|
113
|
+
}));
|
|
114
|
+
const l1 = await repo.load(e1.id);
|
|
115
|
+
expect(l1.int).toBe(maxInt);
|
|
116
|
+
const e2 = await repo.insert(Object.assign(new TypeEntity(), {
|
|
117
|
+
text: 'min', int: minInt, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
|
|
118
|
+
}));
|
|
119
|
+
const l2 = await repo.load(e2.id);
|
|
120
|
+
expect(l2.int).toBe(minInt);
|
|
134
121
|
});
|
|
135
122
|
test('should persist and retrieve float precision', async () => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const loaded = await repo.load(entity.id);
|
|
143
|
-
expect(loaded.float).toBeCloseTo(pi);
|
|
144
|
-
});
|
|
123
|
+
const pi = 3.1415926535;
|
|
124
|
+
const entity = await repo.insert(Object.assign(new TypeEntity(), {
|
|
125
|
+
text: 'pi', int: 0, float: pi, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
|
|
126
|
+
}));
|
|
127
|
+
const loaded = await repo.load(entity.id);
|
|
128
|
+
expect(loaded.float).toBeCloseTo(pi);
|
|
145
129
|
});
|
|
146
130
|
test('should persist and retrieve numeric dates', async () => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const loaded = await repo.load(entity.id);
|
|
154
|
-
expect(loaded.numericDate).toBe(today);
|
|
155
|
-
});
|
|
131
|
+
const today = 19723; // 2024-01-01
|
|
132
|
+
const entity = await repo.insert(Object.assign(new TypeEntity(), {
|
|
133
|
+
text: 'date', int: 0, float: 0, bool: true, numericDate: today, timestamp: new Date(), json: {}, tags: [],
|
|
134
|
+
}));
|
|
135
|
+
const loaded = await repo.load(entity.id);
|
|
136
|
+
expect(loaded.numericDate).toBe(today);
|
|
156
137
|
});
|
|
157
138
|
test('should persist and retrieve timestamps', async () => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
expect(typeof loaded.timestamp).toBe('number');
|
|
169
|
-
expect(Math.abs(loaded.timestamp - now.getTime())).toBeLessThan(2);
|
|
170
|
-
});
|
|
139
|
+
const now = new Date();
|
|
140
|
+
const entity = await repo.insert(Object.assign(new TypeEntity(), {
|
|
141
|
+
text: 'ts', int: 0, float: 0, bool: true, numericDate: 0, timestamp: now, json: {}, tags: [],
|
|
142
|
+
}));
|
|
143
|
+
const loaded = await repo.load(entity.id);
|
|
144
|
+
// Postgres stores microsecond precision, JS Date has millisecond.
|
|
145
|
+
// Drizzle custom type returns number (milliseconds).
|
|
146
|
+
// We accept a small difference due to precision loss or round-trip.
|
|
147
|
+
expect(typeof loaded.timestamp).toBe('number');
|
|
148
|
+
expect(Math.abs(loaded.timestamp - now.getTime())).toBeLessThan(2);
|
|
171
149
|
});
|
|
172
150
|
test('should persist and retrieve complex JSON', async () => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const loaded = await repo.load(entity.id);
|
|
184
|
-
expect(loaded.json).toEqual(data);
|
|
185
|
-
});
|
|
151
|
+
const data = {
|
|
152
|
+
foo: 'bar',
|
|
153
|
+
nested: { a: 1, b: [true, false] },
|
|
154
|
+
nullVal: null,
|
|
155
|
+
};
|
|
156
|
+
const entity = await repo.insert(Object.assign(new TypeEntity(), {
|
|
157
|
+
text: 'json', int: 0, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: data, tags: [],
|
|
158
|
+
}));
|
|
159
|
+
const loaded = await repo.load(entity.id);
|
|
160
|
+
expect(loaded.json).toEqual(data);
|
|
186
161
|
});
|
|
187
162
|
test('should persist and retrieve arrays', async () => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const loaded = await repo.load(entity.id);
|
|
195
|
-
expect(loaded.tags).toEqual(tags);
|
|
196
|
-
});
|
|
163
|
+
const tags = ['a', 'b', 'c'];
|
|
164
|
+
const entity = await repo.insert(Object.assign(new TypeEntity(), {
|
|
165
|
+
text: 'array', int: 0, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags,
|
|
166
|
+
}));
|
|
167
|
+
const loaded = await repo.load(entity.id);
|
|
168
|
+
expect(loaded.tags).toEqual(tags);
|
|
197
169
|
});
|
|
198
170
|
test('should persist and retrieve empty arrays', async () => {
|
|
199
|
-
await
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const loaded = await repo.load(entity.id);
|
|
205
|
-
expect(loaded.tags).toEqual([]);
|
|
206
|
-
});
|
|
171
|
+
const entity = await repo.insert(Object.assign(new TypeEntity(), {
|
|
172
|
+
text: 'empty-array', int: 0, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
|
|
173
|
+
}));
|
|
174
|
+
const loaded = await repo.load(entity.id);
|
|
175
|
+
expect(loaded.tags).toEqual([]);
|
|
207
176
|
});
|
|
208
177
|
test('should fail on integer overflow', async () => {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
text: 'overflow', int: overflow, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
|
|
215
|
-
}))).rejects.toThrow(); // Postgres should reject
|
|
216
|
-
});
|
|
178
|
+
// Max int is 2147483647
|
|
179
|
+
const overflow = 3000000000;
|
|
180
|
+
await expect(repo.insert(Object.assign(new TypeEntity(), {
|
|
181
|
+
text: 'overflow', int: overflow, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
|
|
182
|
+
}))).rejects.toThrow(); // Postgres should reject
|
|
217
183
|
});
|
|
218
184
|
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: <explanation> */
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
import { sql } from 'drizzle-orm';
|
|
12
|
+
import { beforeAll, describe, expect, test } from 'vitest';
|
|
13
|
+
import { StringProperty } from '../../schema/index.js';
|
|
14
|
+
import { setupIntegrationTest } from '../../testing/index.js';
|
|
15
|
+
import { ChildEntity, Column, Inheritance, Table } from '../decorators.js';
|
|
16
|
+
import { Entity } from '../entity.js';
|
|
17
|
+
import { getRepository } from '../server/index.js';
|
|
18
|
+
describe('ORM Repository Undelete (Integration)', () => {
|
|
19
|
+
let injector;
|
|
20
|
+
let db;
|
|
21
|
+
let repository;
|
|
22
|
+
let baseRepo;
|
|
23
|
+
let subRepo;
|
|
24
|
+
const schema = 'test_orm_undelete';
|
|
25
|
+
let UndeleteEntity = class UndeleteEntity extends Entity {
|
|
26
|
+
name;
|
|
27
|
+
};
|
|
28
|
+
__decorate([
|
|
29
|
+
StringProperty(),
|
|
30
|
+
__metadata("design:type", String)
|
|
31
|
+
], UndeleteEntity.prototype, "name", void 0);
|
|
32
|
+
UndeleteEntity = __decorate([
|
|
33
|
+
Table('undelete_entities', { schema })
|
|
34
|
+
], UndeleteEntity);
|
|
35
|
+
let Base = class Base extends Entity {
|
|
36
|
+
type;
|
|
37
|
+
baseName;
|
|
38
|
+
};
|
|
39
|
+
__decorate([
|
|
40
|
+
StringProperty(),
|
|
41
|
+
Column({ name: 'type' }),
|
|
42
|
+
__metadata("design:type", String)
|
|
43
|
+
], Base.prototype, "type", void 0);
|
|
44
|
+
__decorate([
|
|
45
|
+
StringProperty(),
|
|
46
|
+
__metadata("design:type", String)
|
|
47
|
+
], Base.prototype, "baseName", void 0);
|
|
48
|
+
Base = __decorate([
|
|
49
|
+
Table('bases', { schema }),
|
|
50
|
+
Inheritance({ strategy: 'joined', discriminatorColumn: 'type' })
|
|
51
|
+
], Base);
|
|
52
|
+
let Subtype = class Subtype extends Base {
|
|
53
|
+
subData;
|
|
54
|
+
};
|
|
55
|
+
__decorate([
|
|
56
|
+
StringProperty(),
|
|
57
|
+
__metadata("design:type", String)
|
|
58
|
+
], Subtype.prototype, "subData", void 0);
|
|
59
|
+
Subtype = __decorate([
|
|
60
|
+
Table('subtypes', { schema }),
|
|
61
|
+
ChildEntity('subtype')
|
|
62
|
+
], Subtype);
|
|
63
|
+
beforeAll(async () => {
|
|
64
|
+
({ injector, database: db } = await setupIntegrationTest({ orm: { schema } }));
|
|
65
|
+
repository = injector.resolve(getRepository(UndeleteEntity));
|
|
66
|
+
baseRepo = injector.resolve(getRepository(Base));
|
|
67
|
+
subRepo = injector.resolve(getRepository(Subtype));
|
|
68
|
+
await db.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
|
|
69
|
+
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('undelete_entities')} CASCADE`);
|
|
70
|
+
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('subtypes')} CASCADE`);
|
|
71
|
+
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('bases')} CASCADE`);
|
|
72
|
+
await db.execute(sql `
|
|
73
|
+
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('undelete_entities')} (
|
|
74
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
75
|
+
name TEXT NOT NULL,
|
|
76
|
+
revision INTEGER NOT NULL,
|
|
77
|
+
revision_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
78
|
+
create_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
79
|
+
delete_timestamp TIMESTAMP WITH TIME ZONE,
|
|
80
|
+
attributes JSONB NOT NULL DEFAULT '{}'
|
|
81
|
+
)
|
|
82
|
+
`);
|
|
83
|
+
await db.execute(sql `
|
|
84
|
+
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('bases')} (
|
|
85
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
86
|
+
type TEXT NOT NULL,
|
|
87
|
+
base_name TEXT NOT NULL,
|
|
88
|
+
revision INTEGER NOT NULL,
|
|
89
|
+
revision_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
90
|
+
create_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
91
|
+
delete_timestamp TIMESTAMP WITH TIME ZONE,
|
|
92
|
+
attributes JSONB NOT NULL DEFAULT '{}',
|
|
93
|
+
UNIQUE (id, type)
|
|
94
|
+
)
|
|
95
|
+
`);
|
|
96
|
+
await db.execute(sql `
|
|
97
|
+
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('subtypes')} (
|
|
98
|
+
id UUID PRIMARY KEY,
|
|
99
|
+
type TEXT NOT NULL CHECK (type = 'subtype'),
|
|
100
|
+
sub_data TEXT NOT NULL,
|
|
101
|
+
FOREIGN KEY (id, type) REFERENCES ${sql.identifier(schema)}.${sql.identifier('bases')} (id, type) ON DELETE CASCADE
|
|
102
|
+
)
|
|
103
|
+
`);
|
|
104
|
+
});
|
|
105
|
+
test('should undelete an entity by ID', async () => {
|
|
106
|
+
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('undelete_entities')} CASCADE`);
|
|
107
|
+
const e1 = await repository.insert(Object.assign(new UndeleteEntity(), { name: 'E1' }));
|
|
108
|
+
await repository.delete(e1.id);
|
|
109
|
+
expect(await repository.has(e1.id)).toBe(false);
|
|
110
|
+
const undeleted = await repository.undelete(e1.id);
|
|
111
|
+
expect(undeleted.id).toBe(e1.id);
|
|
112
|
+
expect(undeleted.metadata.deleteTimestamp).toBeNull();
|
|
113
|
+
expect(undeleted.metadata.revision).toBe(3); // Insert (1) -> Delete (2) -> Undelete (3)
|
|
114
|
+
expect(await repository.has(e1.id)).toBe(true);
|
|
115
|
+
const loaded = await repository.load(e1.id);
|
|
116
|
+
expect(loaded.name).toBe('E1');
|
|
117
|
+
expect(loaded.metadata.deleteTimestamp).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
test('should undelete multiple entities by IDs', async () => {
|
|
120
|
+
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('undelete_entities')} CASCADE`);
|
|
121
|
+
const e1 = await repository.insert(Object.assign(new UndeleteEntity(), { name: 'E1' }));
|
|
122
|
+
const e2 = await repository.insert(Object.assign(new UndeleteEntity(), { name: 'E2' }));
|
|
123
|
+
await repository.deleteMany([e1.id, e2.id]);
|
|
124
|
+
const undeleted = await repository.undeleteMany([e1.id, e2.id]);
|
|
125
|
+
expect(undeleted).toHaveLength(2);
|
|
126
|
+
expect(await repository.count()).toBe(2);
|
|
127
|
+
});
|
|
128
|
+
test('should undelete entities by query', async () => {
|
|
129
|
+
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('undelete_entities')} CASCADE`);
|
|
130
|
+
const e1 = await repository.insert(Object.assign(new UndeleteEntity(), { name: 'A' }));
|
|
131
|
+
const e2 = await repository.insert(Object.assign(new UndeleteEntity(), { name: 'B' }));
|
|
132
|
+
const e3 = await repository.insert(Object.assign(new UndeleteEntity(), { name: 'A' }));
|
|
133
|
+
await repository.deleteManyByQuery({ name: 'A' });
|
|
134
|
+
expect(await repository.count()).toBe(1);
|
|
135
|
+
const undeleted = await repository.undeleteManyByQuery({ name: 'A' });
|
|
136
|
+
expect(undeleted).toHaveLength(2);
|
|
137
|
+
expect(await repository.count()).toBe(3);
|
|
138
|
+
});
|
|
139
|
+
test('should ignore already active entities when undeleting', async () => {
|
|
140
|
+
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('undelete_entities')} CASCADE`);
|
|
141
|
+
const e1 = await repository.insert(Object.assign(new UndeleteEntity(), { name: 'E1' }));
|
|
142
|
+
const result = await repository.tryUndelete(e1.id);
|
|
143
|
+
expect(result).toBeUndefined();
|
|
144
|
+
expect(await repository.count()).toBe(1);
|
|
145
|
+
});
|
|
146
|
+
test('should support metadata updates during undeletion', async () => {
|
|
147
|
+
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('undelete_entities')} CASCADE`);
|
|
148
|
+
const e1 = await repository.insert(Object.assign(new UndeleteEntity(), { name: 'E1' }));
|
|
149
|
+
await repository.delete(e1.id);
|
|
150
|
+
const undeleted = await repository.undelete(e1.id, {}, { attributes: { restored: true } });
|
|
151
|
+
expect(undeleted.metadata.attributes).toEqual({ restored: true });
|
|
152
|
+
const loaded = await repository.load(e1.id);
|
|
153
|
+
expect(loaded.metadata.attributes).toEqual({ restored: true });
|
|
154
|
+
});
|
|
155
|
+
test('should throw NotFoundError if entity does not exist or is not deleted', async () => {
|
|
156
|
+
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('undelete_entities')} CASCADE`);
|
|
157
|
+
await expect(repository.undelete('00000000-0000-0000-0000-000000000000')).rejects.toThrow();
|
|
158
|
+
const e1 = await repository.insert(Object.assign(new UndeleteEntity(), { name: 'E1' }));
|
|
159
|
+
await expect(repository.undelete(e1.id)).rejects.toThrow();
|
|
160
|
+
});
|
|
161
|
+
test('should undelete from subtype repository (CTI)', async () => {
|
|
162
|
+
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('bases')} CASCADE`);
|
|
163
|
+
const entity = await subRepo.insert(Object.assign(new Subtype(), { baseName: 'B1', subData: 'S1' }));
|
|
164
|
+
await subRepo.delete(entity.id);
|
|
165
|
+
expect(await subRepo.has(entity.id)).toBe(false);
|
|
166
|
+
const undeleted = await subRepo.undelete(entity.id);
|
|
167
|
+
expect(undeleted.id).toBe(entity.id);
|
|
168
|
+
expect(undeleted.metadata.deleteTimestamp).toBeNull();
|
|
169
|
+
expect(undeleted.metadata.revision).toBe(3);
|
|
170
|
+
expect(await subRepo.has(entity.id)).toBe(true);
|
|
171
|
+
const loaded = await subRepo.load(entity.id);
|
|
172
|
+
expect(loaded.baseName).toBe('B1');
|
|
173
|
+
expect(loaded.subData).toBe('S1');
|
|
174
|
+
});
|
|
175
|
+
test('should undelete polymorphically from base repository (CTI)', async () => {
|
|
176
|
+
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('bases')} CASCADE`);
|
|
177
|
+
const entity = await subRepo.insert(Object.assign(new Subtype(), { baseName: 'B1', subData: 'S1' }));
|
|
178
|
+
await subRepo.delete(entity.id);
|
|
179
|
+
expect(await baseRepo.has(entity.id)).toBe(false);
|
|
180
|
+
const undeleted = await baseRepo.undelete(entity.id);
|
|
181
|
+
expect(undeleted.id).toBe(entity.id);
|
|
182
|
+
expect(undeleted.metadata.deleteTimestamp).toBeNull();
|
|
183
|
+
expect(await baseRepo.has(entity.id)).toBe(true);
|
|
184
|
+
const loaded = await baseRepo.load(entity.id, { includeSubclasses: true });
|
|
185
|
+
expect(loaded).toBeInstanceOf(Subtype);
|
|
186
|
+
expect(loaded.subData).toBe('S1');
|
|
187
|
+
});
|
|
188
|
+
test('should undelete multiple entities polymorphically (CTI)', async () => {
|
|
189
|
+
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('bases')} CASCADE`);
|
|
190
|
+
const e1 = await subRepo.insert(Object.assign(new Subtype(), { baseName: 'B1', subData: 'S1' }));
|
|
191
|
+
const e2 = await subRepo.insert(Object.assign(new Subtype(), { baseName: 'B2', subData: 'S2' }));
|
|
192
|
+
await baseRepo.deleteMany([e1.id, e2.id]);
|
|
193
|
+
expect(await baseRepo.count()).toBe(0);
|
|
194
|
+
const undeleted = await baseRepo.undeleteMany([e1.id, e2.id]);
|
|
195
|
+
expect(undeleted).toHaveLength(2);
|
|
196
|
+
expect(await baseRepo.count()).toBe(2);
|
|
197
|
+
const loadedAll = await baseRepo.loadAll({ includeSubclasses: true });
|
|
198
|
+
expect(loadedAll).toHaveLength(2);
|
|
199
|
+
expect(loadedAll.every((e) => e instanceof Subtype)).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
});
|