@tstdl/base 0.93.140 → 0.93.142
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/application/application.d.ts +1 -1
- package/application/application.js +1 -1
- package/application/providers.d.ts +20 -2
- package/application/providers.js +34 -7
- package/audit/module.d.ts +5 -0
- package/audit/module.js +9 -1
- package/authentication/client/authentication.service.d.ts +1 -0
- package/authentication/client/authentication.service.js +3 -2
- package/authentication/server/module.d.ts +5 -0
- package/authentication/server/module.js +9 -1
- package/authentication/tests/authentication.api-controller.test.js +1 -1
- package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
- package/authentication/tests/authentication.client-service.test.js +1 -1
- package/circuit-breaker/circuit-breaker.d.ts +6 -4
- package/circuit-breaker/postgres/circuit-breaker.d.ts +1 -0
- package/circuit-breaker/postgres/circuit-breaker.js +8 -5
- package/circuit-breaker/postgres/module.d.ts +1 -0
- package/circuit-breaker/postgres/module.js +5 -1
- package/circuit-breaker/tests/circuit-breaker.test.js +20 -0
- package/document-management/server/configure.js +5 -1
- package/document-management/server/module.d.ts +1 -1
- package/document-management/server/module.js +1 -1
- package/document-management/server/services/document-management-ancillary.service.js +1 -1
- package/document-management/tests/ai-config-hierarchy.test.js +0 -5
- package/document-management/tests/document-management-ai-overrides.test.js +0 -1
- package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
- package/examples/document-management/main.d.ts +1 -0
- package/examples/document-management/main.js +14 -11
- package/key-value-store/postgres/module.d.ts +1 -0
- package/key-value-store/postgres/module.js +5 -1
- package/lock/postgres/module.d.ts +1 -0
- package/lock/postgres/module.js +5 -1
- package/mail/module.d.ts +5 -1
- package/mail/module.js +11 -6
- package/module/modules/web-server.module.js +2 -3
- package/notification/server/module.d.ts +1 -0
- package/notification/server/module.js +5 -1
- package/notification/tests/notification-api.test.js +5 -1
- package/notification/tests/notification-flow.test.js +8 -5
- package/orm/decorators.d.ts +22 -5
- package/orm/decorators.js +10 -1
- package/orm/server/bootstrap.d.ts +11 -0
- package/orm/server/bootstrap.js +31 -0
- package/orm/server/drizzle/schema-converter.d.ts +3 -1
- package/orm/server/drizzle/schema-converter.js +85 -56
- package/orm/server/encryption.d.ts +0 -1
- package/orm/server/encryption.js +1 -4
- package/orm/server/extension.d.ts +14 -0
- package/orm/server/extension.js +27 -0
- package/orm/server/index.d.ts +3 -6
- package/orm/server/index.js +3 -6
- package/orm/server/migration.d.ts +18 -0
- package/orm/server/migration.js +58 -0
- package/orm/server/repository.d.ts +2 -1
- package/orm/server/repository.js +19 -9
- package/orm/server/transaction.d.ts +6 -10
- package/orm/server/transaction.js +25 -26
- package/orm/server/transactional.js +3 -3
- package/orm/tests/database-extension.test.js +63 -0
- package/orm/tests/database-migration.test.js +83 -0
- package/orm/tests/encryption.test.js +3 -4
- package/orm/tests/repository-compound-primary-key.test.d.ts +2 -0
- package/orm/tests/repository-compound-primary-key.test.js +234 -0
- package/orm/tests/schema-generation.test.d.ts +1 -0
- package/orm/tests/schema-generation.test.js +52 -5
- package/orm/utils.d.ts +17 -2
- package/orm/utils.js +49 -1
- package/package.json +5 -4
- package/rate-limit/postgres/module.d.ts +1 -0
- package/rate-limit/postgres/module.js +5 -1
- package/reflection/decorator-data.js +11 -12
- package/task-queue/README.md +2 -10
- package/task-queue/postgres/drizzle/0000_great_gwen_stacy.sql +84 -0
- package/task-queue/postgres/drizzle/meta/0000_snapshot.json +250 -89
- package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
- package/task-queue/postgres/module.d.ts +1 -0
- package/task-queue/postgres/module.js +6 -1
- package/task-queue/postgres/schemas.d.ts +15 -6
- package/task-queue/postgres/schemas.js +4 -3
- package/task-queue/postgres/task-queue.d.ts +18 -15
- package/task-queue/postgres/task-queue.js +797 -499
- package/task-queue/postgres/task.model.d.ts +20 -9
- package/task-queue/postgres/task.model.js +65 -39
- package/task-queue/task-context.d.ts +12 -7
- package/task-queue/task-context.js +8 -6
- package/task-queue/task-queue.d.ts +364 -43
- package/task-queue/task-queue.js +153 -41
- package/task-queue/tests/coverage-branch.test.d.ts +1 -0
- package/task-queue/tests/coverage-branch.test.js +395 -0
- package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
- package/task-queue/tests/coverage-enhancement.test.js +150 -0
- package/task-queue/tests/dag.test.d.ts +1 -0
- package/task-queue/tests/dag.test.js +188 -0
- package/task-queue/tests/dependencies.test.js +165 -47
- package/task-queue/tests/enqueue-batch.test.d.ts +1 -0
- package/task-queue/tests/enqueue-batch.test.js +125 -0
- package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
- package/task-queue/tests/fan-out-spawning.test.js +94 -0
- package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
- package/task-queue/tests/idempotent-replacement.test.js +114 -0
- package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
- package/task-queue/tests/missing-idempotent-tasks.test.js +39 -0
- package/task-queue/tests/queue.test.js +294 -49
- package/task-queue/tests/shutdown.test.d.ts +1 -0
- package/task-queue/tests/shutdown.test.js +41 -0
- package/task-queue/tests/transactions.test.d.ts +1 -0
- package/task-queue/tests/transactions.test.js +47 -0
- package/task-queue/tests/worker.test.js +63 -15
- package/task-queue/tests/zombie-parent.test.d.ts +1 -0
- package/task-queue/tests/zombie-parent.test.js +45 -0
- package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
- package/task-queue/tests/zombie-recovery.test.js +51 -0
- package/test5.js +5 -5
- package/testing/integration-setup.d.ts +4 -4
- package/testing/integration-setup.js +56 -29
- package/text/localization.service.js +2 -2
- package/utils/file-reader.js +1 -2
- package/utils/timing.d.ts +2 -2
- package/task-queue/postgres/drizzle/0000_simple_invisible_woman.sql +0 -74
- package/task-queue/tests/complex.test.js +0 -306
- package/task-queue/tests/extensive-dependencies.test.js +0 -234
- /package/{task-queue/tests/complex.test.d.ts → orm/tests/database-extension.test.d.ts} +0 -0
- /package/{task-queue/tests/extensive-dependencies.test.d.ts → orm/tests/database-migration.test.d.ts} +0 -0
|
@@ -0,0 +1,234 @@
|
|
|
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 { Injector, runInInjectionContext } from '../../injector/index.js';
|
|
12
|
+
import { StringProperty } from '../../schema/index.js';
|
|
13
|
+
import { dropTables, setupIntegrationTest } from '../../testing/index.js';
|
|
14
|
+
import { sql } from 'drizzle-orm';
|
|
15
|
+
import { getTableConfig } from 'drizzle-orm/pg-core';
|
|
16
|
+
import { beforeAll, beforeEach, describe, expect, test } from 'vitest';
|
|
17
|
+
import { ChildEntity, Inheritance, PrimaryKey, Table } from '../decorators.js';
|
|
18
|
+
import { Entity } from '../entity.js';
|
|
19
|
+
import { getDrizzleTableFromType } from '../server/drizzle/index.js';
|
|
20
|
+
import { Database, injectRepository } from '../server/index.js';
|
|
21
|
+
describe('ORM Repository Compound Primary Key (Integration)', () => {
|
|
22
|
+
let injector;
|
|
23
|
+
let database;
|
|
24
|
+
const schema = 'test_orm_compound_pk';
|
|
25
|
+
let CompoundEntity = class CompoundEntity extends Entity {
|
|
26
|
+
namespace;
|
|
27
|
+
data;
|
|
28
|
+
};
|
|
29
|
+
__decorate([
|
|
30
|
+
StringProperty(),
|
|
31
|
+
__metadata("design:type", String)
|
|
32
|
+
], CompoundEntity.prototype, "namespace", void 0);
|
|
33
|
+
__decorate([
|
|
34
|
+
StringProperty(),
|
|
35
|
+
__metadata("design:type", String)
|
|
36
|
+
], CompoundEntity.prototype, "data", void 0);
|
|
37
|
+
CompoundEntity = __decorate([
|
|
38
|
+
Table('compound_entities', { schema }),
|
|
39
|
+
PrimaryKey(['namespace', 'id'])
|
|
40
|
+
], CompoundEntity);
|
|
41
|
+
let ParentEntity = class ParentEntity extends Entity {
|
|
42
|
+
namespace;
|
|
43
|
+
type;
|
|
44
|
+
};
|
|
45
|
+
__decorate([
|
|
46
|
+
StringProperty(),
|
|
47
|
+
__metadata("design:type", String)
|
|
48
|
+
], ParentEntity.prototype, "namespace", void 0);
|
|
49
|
+
__decorate([
|
|
50
|
+
StringProperty(),
|
|
51
|
+
__metadata("design:type", String)
|
|
52
|
+
], ParentEntity.prototype, "type", void 0);
|
|
53
|
+
ParentEntity = __decorate([
|
|
54
|
+
Table('parent_entities', { schema }),
|
|
55
|
+
PrimaryKey(['namespace', 'id']),
|
|
56
|
+
Inheritance({ strategy: 'joined', discriminatorColumn: 'type' })
|
|
57
|
+
], ParentEntity);
|
|
58
|
+
let ChildEntityModel = class ChildEntityModel extends ParentEntity {
|
|
59
|
+
childData;
|
|
60
|
+
};
|
|
61
|
+
__decorate([
|
|
62
|
+
StringProperty(),
|
|
63
|
+
__metadata("design:type", String)
|
|
64
|
+
], ChildEntityModel.prototype, "childData", void 0);
|
|
65
|
+
ChildEntityModel = __decorate([
|
|
66
|
+
Table('child_entities', { schema }),
|
|
67
|
+
ChildEntity('child')
|
|
68
|
+
], ChildEntityModel);
|
|
69
|
+
let GrandchildEntity = class GrandchildEntity extends ChildEntityModel {
|
|
70
|
+
grandchildData;
|
|
71
|
+
};
|
|
72
|
+
__decorate([
|
|
73
|
+
StringProperty(),
|
|
74
|
+
__metadata("design:type", String)
|
|
75
|
+
], GrandchildEntity.prototype, "grandchildData", void 0);
|
|
76
|
+
GrandchildEntity = __decorate([
|
|
77
|
+
Table('grandchild_entities', { schema }),
|
|
78
|
+
ChildEntity('grandchild')
|
|
79
|
+
], GrandchildEntity);
|
|
80
|
+
beforeAll(async () => {
|
|
81
|
+
({ injector, database } = await setupIntegrationTest({ orm: { schema } }));
|
|
82
|
+
await database.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
|
|
83
|
+
await dropTables(database, schema, ['grandchild_entities', 'child_entities', 'parent_entities', 'compound_entities']);
|
|
84
|
+
await database.execute(sql `
|
|
85
|
+
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('compound_entities')} (
|
|
86
|
+
namespace TEXT NOT NULL,
|
|
87
|
+
id UUID NOT NULL,
|
|
88
|
+
data TEXT NOT NULL,
|
|
89
|
+
revision INTEGER NOT NULL,
|
|
90
|
+
revision_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
91
|
+
create_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
92
|
+
delete_timestamp TIMESTAMP WITH TIME ZONE,
|
|
93
|
+
attributes JSONB NOT NULL DEFAULT '{}',
|
|
94
|
+
PRIMARY KEY (namespace, id)
|
|
95
|
+
)
|
|
96
|
+
`);
|
|
97
|
+
await database.execute(sql `
|
|
98
|
+
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('parent_entities')} (
|
|
99
|
+
namespace TEXT NOT NULL,
|
|
100
|
+
id UUID NOT NULL,
|
|
101
|
+
type TEXT NOT NULL,
|
|
102
|
+
revision INTEGER NOT NULL,
|
|
103
|
+
revision_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
104
|
+
create_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
105
|
+
delete_timestamp TIMESTAMP WITH TIME ZONE,
|
|
106
|
+
attributes JSONB NOT NULL DEFAULT '{}',
|
|
107
|
+
PRIMARY KEY (namespace, id),
|
|
108
|
+
UNIQUE (namespace, id, type)
|
|
109
|
+
)
|
|
110
|
+
`);
|
|
111
|
+
await database.execute(sql `
|
|
112
|
+
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('child_entities')} (
|
|
113
|
+
namespace TEXT NOT NULL,
|
|
114
|
+
id UUID NOT NULL,
|
|
115
|
+
type TEXT NOT NULL CHECK (type IN ('child', 'grandchild')),
|
|
116
|
+
child_data TEXT NOT NULL,
|
|
117
|
+
PRIMARY KEY (namespace, id),
|
|
118
|
+
FOREIGN KEY (namespace, id, type) REFERENCES ${sql.identifier(schema)}.${sql.identifier('parent_entities')} (namespace, id, type) ON DELETE CASCADE,
|
|
119
|
+
UNIQUE (namespace, id, type)
|
|
120
|
+
)
|
|
121
|
+
`);
|
|
122
|
+
await database.execute(sql `
|
|
123
|
+
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('grandchild_entities')} (
|
|
124
|
+
namespace TEXT NOT NULL,
|
|
125
|
+
id UUID NOT NULL,
|
|
126
|
+
type TEXT NOT NULL CHECK (type = 'grandchild'),
|
|
127
|
+
grandchild_data TEXT NOT NULL,
|
|
128
|
+
PRIMARY KEY (namespace, id),
|
|
129
|
+
FOREIGN KEY (namespace, id, type) REFERENCES ${sql.identifier(schema)}.${sql.identifier('child_entities')} (namespace, id, type) ON DELETE CASCADE
|
|
130
|
+
)
|
|
131
|
+
`);
|
|
132
|
+
});
|
|
133
|
+
beforeEach(async () => {
|
|
134
|
+
await database.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('grandchild_entities')}, ${sql.identifier(schema)}.${sql.identifier('child_entities')}, ${sql.identifier(schema)}.${sql.identifier('parent_entities')}, ${sql.identifier(schema)}.${sql.identifier('compound_entities')} CASCADE`);
|
|
135
|
+
});
|
|
136
|
+
test('should insert and load compound entity', async () => {
|
|
137
|
+
await runInInjectionContext(injector, async () => {
|
|
138
|
+
const repository = injectRepository(CompoundEntity);
|
|
139
|
+
const entity = new CompoundEntity();
|
|
140
|
+
entity.id = '00000000-0000-0000-0000-000000000001';
|
|
141
|
+
entity.namespace = 'ns1';
|
|
142
|
+
entity.data = 'test data';
|
|
143
|
+
await repository.insert(entity);
|
|
144
|
+
const loaded = await repository.loadByQuery({ namespace: 'ns1', id: '00000000-0000-0000-0000-000000000001' });
|
|
145
|
+
expect(loaded.data).toBe('test data');
|
|
146
|
+
expect(loaded.namespace).toBe('ns1');
|
|
147
|
+
expect(loaded.id).toBe('00000000-0000-0000-0000-000000000001');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
test('should upsert compound entity', async () => {
|
|
151
|
+
await runInInjectionContext(injector, async () => {
|
|
152
|
+
const repository = injectRepository(CompoundEntity);
|
|
153
|
+
const entity = new CompoundEntity();
|
|
154
|
+
entity.id = '00000000-0000-0000-0000-000000000010';
|
|
155
|
+
entity.namespace = 'ns1';
|
|
156
|
+
entity.data = 'initial data';
|
|
157
|
+
await repository.insert(entity);
|
|
158
|
+
const updateEntity = new CompoundEntity();
|
|
159
|
+
updateEntity.id = '00000000-0000-0000-0000-000000000010';
|
|
160
|
+
updateEntity.namespace = 'ns1';
|
|
161
|
+
updateEntity.data = 'updated data';
|
|
162
|
+
await repository.upsert(['namespace', 'id'], updateEntity);
|
|
163
|
+
const loaded = await repository.loadByQuery({ namespace: 'ns1', id: '00000000-0000-0000-0000-000000000010' });
|
|
164
|
+
expect(loaded.data).toBe('updated data');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
test('should insert and load child entity with compound PK', async () => {
|
|
168
|
+
await runInInjectionContext(injector, async () => {
|
|
169
|
+
const repository = injectRepository(ChildEntityModel);
|
|
170
|
+
const entity = new ChildEntityModel();
|
|
171
|
+
entity.id = '00000000-0000-0000-0000-000000000020';
|
|
172
|
+
entity.namespace = 'ns2';
|
|
173
|
+
entity.type = 'child';
|
|
174
|
+
entity.childData = 'child specific data';
|
|
175
|
+
await repository.insert(entity);
|
|
176
|
+
const loaded = await repository.loadByQuery({ namespace: 'ns2', id: '00000000-0000-0000-0000-000000000020' });
|
|
177
|
+
expect(loaded.childData).toBe('child specific data');
|
|
178
|
+
expect(loaded.namespace).toBe('ns2');
|
|
179
|
+
expect(loaded.id).toBe('00000000-0000-0000-0000-000000000020');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
test('should insert and load grandchild entity with compound PK', async () => {
|
|
183
|
+
await runInInjectionContext(injector, async () => {
|
|
184
|
+
const repository = injectRepository(GrandchildEntity);
|
|
185
|
+
const entity = new GrandchildEntity();
|
|
186
|
+
entity.id = '00000000-0000-0000-0000-000000000050';
|
|
187
|
+
entity.namespace = 'ns5';
|
|
188
|
+
entity.type = 'grandchild';
|
|
189
|
+
entity.childData = 'child part';
|
|
190
|
+
entity.grandchildData = 'grandchild part';
|
|
191
|
+
await repository.insert(entity);
|
|
192
|
+
const loaded = await repository.loadByQuery({ namespace: 'ns5', id: '00000000-0000-0000-0000-000000000050' });
|
|
193
|
+
expect(loaded.grandchildData).toBe('grandchild part');
|
|
194
|
+
expect(loaded.childData).toBe('child part');
|
|
195
|
+
expect(loaded.namespace).toBe('ns5');
|
|
196
|
+
expect(loaded.id).toBe('00000000-0000-0000-0000-000000000050');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
test('should updateManyByQuery with compound PK', async () => {
|
|
200
|
+
await runInInjectionContext(injector, async () => {
|
|
201
|
+
const repository = injectRepository(CompoundEntity);
|
|
202
|
+
const entity1 = new CompoundEntity();
|
|
203
|
+
entity1.id = '00000000-0000-0000-0000-000000000030';
|
|
204
|
+
entity1.namespace = 'ns3';
|
|
205
|
+
entity1.data = 'data 1';
|
|
206
|
+
const entity2 = new CompoundEntity();
|
|
207
|
+
entity2.id = '00000000-0000-0000-0000-000000000040';
|
|
208
|
+
entity2.namespace = 'ns3';
|
|
209
|
+
entity2.data = 'data 2';
|
|
210
|
+
await repository.insertMany([entity1, entity2]);
|
|
211
|
+
await repository.updateManyByQuery({ namespace: 'ns3' }, { data: 'batch updated' });
|
|
212
|
+
const entities = await repository.loadManyByQuery({ namespace: 'ns3' });
|
|
213
|
+
expect(entities).toHaveLength(2);
|
|
214
|
+
expect(entities.every(e => e.data === 'batch updated')).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
test('should respect custom primary key name', () => {
|
|
218
|
+
let CustomPkEntity = class CustomPkEntity extends Entity {
|
|
219
|
+
namespace;
|
|
220
|
+
};
|
|
221
|
+
__decorate([
|
|
222
|
+
StringProperty(),
|
|
223
|
+
__metadata("design:type", String)
|
|
224
|
+
], CustomPkEntity.prototype, "namespace", void 0);
|
|
225
|
+
CustomPkEntity = __decorate([
|
|
226
|
+
Table('custom_pk_table', { schema: 'test' }),
|
|
227
|
+
PrimaryKey(['namespace', 'id'], { name: 'my_custom_pk' })
|
|
228
|
+
], CustomPkEntity);
|
|
229
|
+
const table = getDrizzleTableFromType(CustomPkEntity);
|
|
230
|
+
const config = getTableConfig(table);
|
|
231
|
+
expect(config.primaryKeys).toHaveLength(1);
|
|
232
|
+
expect(config.primaryKeys[0].name).toBe('my_custom_pk');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: <explanation> */
|
|
1
2
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -7,11 +8,11 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
8
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
10
|
};
|
|
10
|
-
import { describe, expect, test } from 'vitest';
|
|
11
11
|
import { getTableConfig } from 'drizzle-orm/pg-core';
|
|
12
|
+
import { describe, expect, test } from 'vitest';
|
|
12
13
|
import { StringProperty } from '../../schema/index.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
14
|
+
import { ChildEntity, Column, Inheritance, ParadeIndex, Table } from '../decorators.js';
|
|
15
|
+
import { Entity, TenantEntity } from '../entity.js';
|
|
15
16
|
import { getDrizzleTableFromType } from '../server/drizzle/schema-converter.js';
|
|
16
17
|
describe('ORM Schema Generation (CTI)', () => {
|
|
17
18
|
test('should generate correct parent table with discriminator and unique constraint', () => {
|
|
@@ -95,6 +96,52 @@ describe('ORM Schema Generation (CTI)', () => {
|
|
|
95
96
|
expect(compositeFk).toBeDefined();
|
|
96
97
|
expect(compositeFk?.reference().columns.map(c => c.name).sort()).toEqual(['id', 'type']);
|
|
97
98
|
});
|
|
99
|
+
test('should generate correct parent and child table for TenantEntity with joined inheritance', () => {
|
|
100
|
+
let BaseTenantUser = class BaseTenantUser extends TenantEntity {
|
|
101
|
+
type;
|
|
102
|
+
};
|
|
103
|
+
__decorate([
|
|
104
|
+
StringProperty(),
|
|
105
|
+
Column({ name: 'type' }),
|
|
106
|
+
__metadata("design:type", String)
|
|
107
|
+
], BaseTenantUser.prototype, "type", void 0);
|
|
108
|
+
BaseTenantUser = __decorate([
|
|
109
|
+
Table('tenant_users', { schema: 'test' }),
|
|
110
|
+
Inheritance({ strategy: 'joined', discriminatorColumn: 'type' })
|
|
111
|
+
], BaseTenantUser);
|
|
112
|
+
let TenantAdmin = class TenantAdmin extends BaseTenantUser {
|
|
113
|
+
role;
|
|
114
|
+
};
|
|
115
|
+
__decorate([
|
|
116
|
+
StringProperty(),
|
|
117
|
+
__metadata("design:type", String)
|
|
118
|
+
], TenantAdmin.prototype, "role", void 0);
|
|
119
|
+
TenantAdmin = __decorate([
|
|
120
|
+
Table('tenant_admins', { schema: 'test' }),
|
|
121
|
+
ChildEntity('admin')
|
|
122
|
+
], TenantAdmin);
|
|
123
|
+
// Test Parent Table
|
|
124
|
+
const parentTable = getDrizzleTableFromType(BaseTenantUser);
|
|
125
|
+
const parentConfig = getTableConfig(parentTable);
|
|
126
|
+
const parentUnique = parentConfig.uniqueConstraints.find((uc) => uc.columns.some((c) => c.name === 'type'));
|
|
127
|
+
expect(parentUnique).toBeDefined();
|
|
128
|
+
// Should be tenant_id, type, id (no duplicates)
|
|
129
|
+
expect(parentUnique.columns.map((c) => c.name)).toEqual(['tenant_id', 'type', 'id']);
|
|
130
|
+
// UNIQUE identifier should use snake_case column names
|
|
131
|
+
expect(parentUnique.name).toBe('tenant_users_tenant_id_type_id_unique');
|
|
132
|
+
// Test Child Table
|
|
133
|
+
const childTable = getDrizzleTableFromType(TenantAdmin);
|
|
134
|
+
const childConfig = getTableConfig(childTable);
|
|
135
|
+
// Child should have tenant_id and id from parent PK, plus type discriminator
|
|
136
|
+
const childColumnNames = childConfig.columns.map((c) => c.name);
|
|
137
|
+
expect(childColumnNames).toContain('tenant_id');
|
|
138
|
+
expect(childColumnNames).toContain('id');
|
|
139
|
+
expect(childColumnNames).toContain('type');
|
|
140
|
+
// Child Foreign Key should link correctly with all 3 columns
|
|
141
|
+
const childFk = childConfig.foreignKeys.find((fk) => fk.reference().columns.length === 3);
|
|
142
|
+
expect(childFk).toBeDefined();
|
|
143
|
+
expect(childFk.reference().columns.map((c) => c.name)).toEqual(['tenant_id', 'type', 'id']);
|
|
144
|
+
});
|
|
98
145
|
test('should generate BM25 index with correct config', () => {
|
|
99
146
|
let SearchableItem = class SearchableItem extends Entity {
|
|
100
147
|
title;
|
|
@@ -112,7 +159,7 @@ describe('ORM Schema Generation (CTI)', () => {
|
|
|
112
159
|
Table('searchable', { schema: 'test' }),
|
|
113
160
|
ParadeIndex({
|
|
114
161
|
columns: ['title', ['body', { tokenizer: 'simple' }]],
|
|
115
|
-
naming: 'abbreviated-table'
|
|
162
|
+
naming: 'abbreviated-table',
|
|
116
163
|
})
|
|
117
164
|
], SearchableItem);
|
|
118
165
|
const table = getDrizzleTableFromType(SearchableItem);
|
|
@@ -122,6 +169,6 @@ describe('ORM Schema Generation (CTI)', () => {
|
|
|
122
169
|
expect(paradeIdx).toBeDefined();
|
|
123
170
|
const extraConfig = paradeIdx.config.with;
|
|
124
171
|
expect(extraConfig).toBeDefined();
|
|
125
|
-
expect(extraConfig.key_field).toBe(
|
|
172
|
+
expect(extraConfig.key_field).toBe(`'id'`);
|
|
126
173
|
});
|
|
127
174
|
});
|
package/orm/utils.d.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { type PropertyMetadata } from '../reflection/registry.js';
|
|
6
6
|
import type { AbstractConstructor } from '../types/index.js';
|
|
7
|
-
import type { InheritanceMetadata } from './decorators.js';
|
|
8
|
-
import type { BaseEntity, Entity } from './entity.js';
|
|
7
|
+
import type { InheritanceMetadata, OrmTableReflectionData } from './decorators.js';
|
|
8
|
+
import type { BaseEntity, Entity, EntityType } from './entity.js';
|
|
9
9
|
/**
|
|
10
10
|
* Converts an array of entities into a Map keyed by entity ID.
|
|
11
11
|
* @template T - The entity type, must extend `Entity` (i.e., have an `id` property).
|
|
@@ -24,3 +24,18 @@ export declare function getEntityIds(entities: (Entity | BaseEntity)[]): string[
|
|
|
24
24
|
export declare function isChildEntity(type: AbstractConstructor): boolean;
|
|
25
25
|
export declare function getInheritanceMetadata(type: AbstractConstructor): InheritanceMetadata | undefined;
|
|
26
26
|
export declare function getOwnProperties(type: AbstractConstructor): PropertyMetadata[];
|
|
27
|
+
/**
|
|
28
|
+
* Gets the database schema name for a given entity type, traversing the inheritance hierarchy.
|
|
29
|
+
* @param type The entity class.
|
|
30
|
+
* @param fallback Optional fallback schema name if none is defined in metadata.
|
|
31
|
+
* @returns The resolved schema name.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getEntitySchema(type: AbstractConstructor, fallback?: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Gets the database table name for a given entity type, traversing the inheritance hierarchy.
|
|
36
|
+
* @param type The entity class.
|
|
37
|
+
* @returns The resolved table name.
|
|
38
|
+
*/
|
|
39
|
+
export declare function getEntityTableName(type: AbstractConstructor): string;
|
|
40
|
+
export declare function getDefaultTableName(type: EntityType): string;
|
|
41
|
+
export declare function getTableReflectionDatas(type: EntityType): OrmTableReflectionData[];
|
package/orm/utils.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Provides utility functions for working with ORM entities.
|
|
4
4
|
*/
|
|
5
5
|
import { reflectionRegistry } from '../reflection/registry.js';
|
|
6
|
-
import {
|
|
6
|
+
import { toSnakeCase } from '../utils/string/index.js';
|
|
7
|
+
import { assertDefined, isDefined, isNotNullOrUndefined, isString, isUndefined } from '../utils/type-guards.js';
|
|
7
8
|
export function getEntityMap(entities, selector = (entity) => entity.id) {
|
|
8
9
|
const entries = entities.map((entity) => [selector(entity), entity]);
|
|
9
10
|
return new Map(entries);
|
|
@@ -36,3 +37,50 @@ export function getOwnProperties(type) {
|
|
|
36
37
|
}
|
|
37
38
|
return [...metadata.properties.values()].filter((property) => !property.inherited || (property.key == 'id'));
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Gets the database schema name for a given entity type, traversing the inheritance hierarchy.
|
|
42
|
+
* @param type The entity class.
|
|
43
|
+
* @param fallback Optional fallback schema name if none is defined in metadata.
|
|
44
|
+
* @returns The resolved schema name.
|
|
45
|
+
*/
|
|
46
|
+
export function getEntitySchema(type, fallback) {
|
|
47
|
+
for (let currentMetadata = reflectionRegistry.getMetadata(type); isNotNullOrUndefined(currentMetadata); currentMetadata = isNotNullOrUndefined(currentMetadata.parent) ? reflectionRegistry.getMetadata(currentMetadata.parent) : undefined) {
|
|
48
|
+
const schema = currentMetadata.data.tryGet('orm')?.schema;
|
|
49
|
+
if (isDefined(schema)) {
|
|
50
|
+
return schema;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (isDefined(fallback)) {
|
|
54
|
+
return fallback;
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`Schema not found for entity ${type.name} and no fallback provided.`);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Gets the database table name for a given entity type, traversing the inheritance hierarchy.
|
|
60
|
+
* @param type The entity class.
|
|
61
|
+
* @returns The resolved table name.
|
|
62
|
+
*/
|
|
63
|
+
export function getEntityTableName(type) {
|
|
64
|
+
for (let currentMetadata = reflectionRegistry.getMetadata(type); isNotNullOrUndefined(currentMetadata); currentMetadata = isNotNullOrUndefined(currentMetadata.parent) ? reflectionRegistry.getMetadata(currentMetadata.parent) : undefined) {
|
|
65
|
+
const name = currentMetadata.data.tryGet('orm')?.name;
|
|
66
|
+
if (isDefined(name)) {
|
|
67
|
+
return name;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return getDefaultTableName(type);
|
|
71
|
+
}
|
|
72
|
+
export function getDefaultTableName(type) {
|
|
73
|
+
return toSnakeCase(isString(type.entityName) ? type.entityName : type.name.replace(/\d+$/u, ''));
|
|
74
|
+
}
|
|
75
|
+
export function getTableReflectionDatas(type) {
|
|
76
|
+
const metadata = reflectionRegistry.getMetadata(type);
|
|
77
|
+
assertDefined(metadata, `Type ${type.name} does not have reflection metadata.`);
|
|
78
|
+
const tableReflectionDatas = [];
|
|
79
|
+
for (let currentMetadata = metadata; isNotNullOrUndefined(currentMetadata?.parent); currentMetadata = reflectionRegistry.getMetadata(currentMetadata.parent)) {
|
|
80
|
+
const tableReflectionData = currentMetadata.data.tryGet('orm');
|
|
81
|
+
if (isDefined(tableReflectionData)) {
|
|
82
|
+
tableReflectionDatas.push(tableReflectionData);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return tableReflectionDatas;
|
|
86
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tstdl/base",
|
|
3
|
-
"version": "0.93.
|
|
3
|
+
"version": "0.93.142",
|
|
4
4
|
"author": "Patrick Hein",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"lint": "eslint --cache source/",
|
|
18
18
|
"pub": "npm run build:production && npm run cleanup:dist && npm publish dist/",
|
|
19
19
|
"test": "vitest run",
|
|
20
|
+
"test:coverage": "f() { vitest run --coverage --coverage.include=\"source/$1/**/*.ts\" source/$1; }; f",
|
|
20
21
|
"tsc:watch": "tsc --watch",
|
|
21
22
|
"tsc-alias:watch": "tsc-alias --watch",
|
|
22
23
|
"cleanup:dist": "rm -vrf dist/tools/",
|
|
@@ -151,8 +152,8 @@
|
|
|
151
152
|
"type-fest": "^5.4"
|
|
152
153
|
},
|
|
153
154
|
"peerDependencies": {
|
|
154
|
-
"@aws-sdk/client-s3": "^3.
|
|
155
|
-
"@aws-sdk/s3-request-presigner": "^3.
|
|
155
|
+
"@aws-sdk/client-s3": "^3.997",
|
|
156
|
+
"@aws-sdk/s3-request-presigner": "^3.997",
|
|
156
157
|
"@genkit-ai/google-genai": "^1.29",
|
|
157
158
|
"@google-cloud/storage": "^7.19",
|
|
158
159
|
"@toon-format/toon": "^2.1.0",
|
|
@@ -167,7 +168,7 @@
|
|
|
167
168
|
"handlebars": "^4.7",
|
|
168
169
|
"mjml": "^4.18",
|
|
169
170
|
"nodemailer": "^8.0",
|
|
170
|
-
"pg": "^8.
|
|
171
|
+
"pg": "^8.19",
|
|
171
172
|
"playwright": "^1.58",
|
|
172
173
|
"preact": "^10.28",
|
|
173
174
|
"preact-render-to-string": "^6.6",
|
|
@@ -2,6 +2,7 @@ import { Injector } from '../../injector/injector.js';
|
|
|
2
2
|
import { type DatabaseConfig } from '../../orm/server/index.js';
|
|
3
3
|
export declare class PostgresRateLimiterModuleConfig {
|
|
4
4
|
database?: DatabaseConfig;
|
|
5
|
+
autoMigrate?: boolean;
|
|
5
6
|
}
|
|
6
7
|
/**
|
|
7
8
|
* configure rate limit module
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { inject } from '../../injector/index.js';
|
|
2
2
|
import { Injector } from '../../injector/injector.js';
|
|
3
|
-
import { Database, migrate } from '../../orm/server/index.js';
|
|
3
|
+
import { Database, migrate, registerDatabaseMigration } from '../../orm/server/index.js';
|
|
4
4
|
import { RateLimiterProvider } from '../provider.js';
|
|
5
5
|
import { RateLimiter } from '../rate-limiter.js';
|
|
6
6
|
import { PostgresRateLimiter } from './postgres-rate-limiter.js';
|
|
7
7
|
import { PostgresRateLimiterProvider } from './rate-limiter.provider.js';
|
|
8
8
|
export class PostgresRateLimiterModuleConfig {
|
|
9
9
|
database;
|
|
10
|
+
autoMigrate;
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
13
|
* configure rate limit module
|
|
@@ -16,6 +17,9 @@ export function configurePostgresRateLimiter({ injector, ...config } = {}) {
|
|
|
16
17
|
targetInjector.register(PostgresRateLimiterModuleConfig, { useValue: config });
|
|
17
18
|
targetInjector.registerSingleton(RateLimiterProvider, { useToken: PostgresRateLimiterProvider });
|
|
18
19
|
targetInjector.registerSingleton(RateLimiter, { useToken: PostgresRateLimiter });
|
|
20
|
+
if (config.autoMigrate != false) {
|
|
21
|
+
registerDatabaseMigration('PostgresRateLimiter', migratePostgresRateLimiterSchema, { injector });
|
|
22
|
+
}
|
|
19
23
|
}
|
|
20
24
|
export async function migratePostgresRateLimiterSchema() {
|
|
21
25
|
const connection = inject(PostgresRateLimiterModuleConfig, undefined, { optional: true })?.database?.connection;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { DetailsError } from '../errors/details.error.js';
|
|
2
2
|
import { isDefined, isFunction, isNumber, isObject, isUndefined } from '../utils/type-guards.js';
|
|
3
3
|
import { getConstructor } from './utils.js';
|
|
4
|
-
// eslint-disable-next-line max-lines-per-function
|
|
5
4
|
export function getDecoratorData(target, propertyKey, descriptorOrParameterIndex) {
|
|
6
5
|
const constructor = getConstructor(target);
|
|
7
6
|
const prototype = constructor.prototype;
|
|
@@ -10,54 +9,54 @@ export function getDecoratorData(target, propertyKey, descriptorOrParameterIndex
|
|
|
10
9
|
return {
|
|
11
10
|
type: 'class',
|
|
12
11
|
constructor,
|
|
13
|
-
prototype
|
|
12
|
+
prototype,
|
|
14
13
|
};
|
|
15
14
|
}
|
|
16
|
-
|
|
15
|
+
if (isDefined(propertyKey) && isUndefined(descriptorOrParameterIndex)) {
|
|
17
16
|
return {
|
|
18
17
|
type: 'property',
|
|
19
18
|
constructor,
|
|
20
19
|
prototype,
|
|
21
20
|
static: isStatic,
|
|
22
|
-
propertyKey
|
|
21
|
+
propertyKey,
|
|
23
22
|
};
|
|
24
23
|
}
|
|
25
|
-
|
|
24
|
+
if (isDefined(propertyKey) && isObject(descriptorOrParameterIndex) && (isFunction(descriptorOrParameterIndex.get ?? descriptorOrParameterIndex.set))) { // eslint-disable-line @typescript-eslint/unbound-method
|
|
26
25
|
return {
|
|
27
26
|
type: 'accessor',
|
|
28
27
|
constructor,
|
|
29
28
|
prototype,
|
|
30
29
|
static: isStatic,
|
|
31
30
|
propertyKey,
|
|
32
|
-
descriptor: descriptorOrParameterIndex
|
|
31
|
+
descriptor: descriptorOrParameterIndex,
|
|
33
32
|
};
|
|
34
33
|
}
|
|
35
|
-
|
|
34
|
+
if (isDefined(propertyKey) && isObject(descriptorOrParameterIndex) && isFunction(descriptorOrParameterIndex.value)) {
|
|
36
35
|
return {
|
|
37
36
|
type: 'method',
|
|
38
37
|
constructor,
|
|
39
38
|
prototype,
|
|
40
39
|
static: isStatic,
|
|
41
40
|
methodKey: propertyKey,
|
|
42
|
-
descriptor: descriptorOrParameterIndex
|
|
41
|
+
descriptor: descriptorOrParameterIndex,
|
|
43
42
|
};
|
|
44
43
|
}
|
|
45
|
-
|
|
44
|
+
if (isDefined(propertyKey) && isNumber(descriptorOrParameterIndex)) {
|
|
46
45
|
return {
|
|
47
46
|
type: 'method-parameter',
|
|
48
47
|
constructor,
|
|
49
48
|
prototype,
|
|
50
49
|
static: isStatic,
|
|
51
50
|
methodKey: propertyKey,
|
|
52
|
-
index: descriptorOrParameterIndex
|
|
51
|
+
index: descriptorOrParameterIndex,
|
|
53
52
|
};
|
|
54
53
|
}
|
|
55
|
-
|
|
54
|
+
if (isNumber(descriptorOrParameterIndex)) {
|
|
56
55
|
return {
|
|
57
56
|
type: 'constructor-parameter',
|
|
58
57
|
constructor,
|
|
59
58
|
prototype,
|
|
60
|
-
index: descriptorOrParameterIndex
|
|
59
|
+
index: descriptorOrParameterIndex,
|
|
61
60
|
};
|
|
62
61
|
}
|
|
63
62
|
throw new DetailsError('unknown type', { type: constructor, isStatic, propertyKey, descriptorOrParameterIndex });
|
package/task-queue/README.md
CHANGED
|
@@ -60,18 +60,11 @@ A function that processes tasks. The `TaskQueue` provides a managed loop (`proce
|
|
|
60
60
|
Register the PostgreSQL backend in your application bootstrap.
|
|
61
61
|
|
|
62
62
|
```typescript
|
|
63
|
-
import { configurePostgresTaskQueue
|
|
64
|
-
import { runInInjectionContext, inject } from '@tstdl/base/injector';
|
|
65
|
-
import { Injector } from '@tstdl/base/injector';
|
|
63
|
+
import { configurePostgresTaskQueue } from '@tstdl/base/task-queue/postgres';
|
|
66
64
|
|
|
67
65
|
export async function bootstrap() {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// Configure the module (uses default DatabaseConfig from context)
|
|
66
|
+
// Configure the module
|
|
71
67
|
configurePostgresTaskQueue();
|
|
72
|
-
|
|
73
|
-
// Run migrations to create the 'task_queue.task' table
|
|
74
|
-
await runInInjectionContext(injector, migratePostgresTaskQueueSchema);
|
|
75
68
|
}
|
|
76
69
|
```
|
|
77
70
|
|
|
@@ -292,7 +285,6 @@ Passed to the worker handler.
|
|
|
292
285
|
| `maxExecutionTime` | `60m` | Hard limit for `Running` state. |
|
|
293
286
|
| `maxTries` | `3` | Maximum dequeue attempts allowed. |
|
|
294
287
|
| `retention` | `30d` | Duration to retain terminal tasks before archival. |
|
|
295
|
-
| `globalConcurrency` | `null` | Max simultaneous running tasks across all workers. |
|
|
296
288
|
| `circuitBreakerThreshold` | `5` | Failures before tripping the circuit breaker. |
|
|
297
289
|
| `retryDelayMinimum` | `5s` | Floor for exponential backoff delay. |
|
|
298
290
|
| `retryDelayMaximum` | `5m` | Ceiling for exponential backoff delay. |
|