@nest-yalc-2/omnikernel-module 2.2.2 → 2.2.4
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/package.json +17 -15
- package/src/__tests__/omni-collection.entity.spec.ts +49 -49
- package/src/__tests__/omni-collection.service.spec.ts +83 -83
- package/src/__tests__/omni-document.entity.spec.ts +55 -55
- package/src/__tests__/omni-document.service.spec.ts +83 -83
- package/src/__tests__/omni-extension-projection-constraints.spec.ts +518 -518
- package/src/__tests__/omni-extension-projection.registration.spec.ts +947 -946
- package/src/__tests__/omni-extension-projection.service.spec.ts +806 -806
- package/src/__tests__/omni-external-ref.entity.spec.ts +36 -36
- package/src/__tests__/omni-external-ref.service.spec.ts +252 -252
- package/src/__tests__/omni-named.entity.spec.ts +33 -33
- package/src/__tests__/omni-record.entity.spec.ts +32 -32
- package/src/__tests__/omni-record.service.spec.ts +107 -107
- package/src/__tests__/omni-relation-semantics.spec.ts +102 -102
- package/src/__tests__/omni-relation.entity.spec.ts +38 -38
- package/src/__tests__/omni-scope-substrate.spec.ts +81 -81
- package/src/__tests__/omni-scoped.backend.spec.ts +115 -115
- package/src/__tests__/omni-scoped.service.spec.ts +354 -354
- package/src/__tests__/omnikernel.diagnostics.spec.ts +94 -94
- package/src/__tests__/omnikernel.module.spec.ts +38 -40
- package/src/__tests__/omnikernel.persistence.spec.ts +154 -154
- package/src/__tests__/omnikernel.public-api.spec.ts +238 -238
- package/src/__tests__/omnikernel.query.service.spec.ts +155 -155
- package/src/base/omni-base.entity.ts +41 -41
- package/src/base/omni-external-ref.entity.ts +53 -53
- package/src/base/omni-named.entity.ts +16 -16
- package/src/base/omni-record.entity.ts +51 -51
- package/src/base/omni-relation.entity.ts +72 -72
- package/src/index.ts +49 -49
- package/src/omni-collection-kind.enum.ts +10 -10
- package/src/omni-collection.backend.ts +16 -16
- package/src/omni-collection.dto.ts +170 -170
- package/src/omni-collection.entity.ts +21 -21
- package/src/omni-collection.service.ts +75 -75
- package/src/omni-document-kind.enum.ts +12 -12
- package/src/omni-document.backend.ts +16 -16
- package/src/omni-document.dto.ts +198 -198
- package/src/omni-document.entity.ts +30 -30
- package/src/omni-document.service.ts +75 -75
- package/src/omni-dto.helpers.ts +21 -21
- package/src/omni-extension-projection.definition.ts +294 -294
- package/src/omni-extension-projection.resource.ts +274 -274
- package/src/omni-extension-projection.service.ts +452 -452
- package/src/omni-external-ref-binding.validator.ts +53 -53
- package/src/omni-external-ref-internal-type.enum.ts +11 -11
- package/src/omni-external-ref.backend.ts +25 -25
- package/src/omni-external-ref.dto.ts +128 -128
- package/src/omni-external-ref.service.ts +255 -255
- package/src/omni-migration.ts +365 -365
- package/src/omni-named.backend.ts +11 -11
- package/src/omni-named.dto.ts +86 -86
- package/src/omni-projection.catalog.ts +345 -345
- package/src/omni-projection.lifecycle.ts +53 -53
- package/src/omni-record-status.enum.ts +11 -11
- package/src/omni-record.backend.ts +19 -19
- package/src/omni-record.dto.ts +157 -157
- package/src/omni-record.service.ts +113 -113
- package/src/omni-relation-kind.contract.ts +49 -49
- package/src/omni-relation-kind.enum.ts +12 -12
- package/src/omni-relation-projection.definition.ts +166 -166
- package/src/omni-relation-projection.resource.ts +360 -360
- package/src/omni-relation-projection.service.ts +610 -610
- package/src/omni-relation-semantics.ts +52 -52
- package/src/omni-relation-status.enum.ts +11 -11
- package/src/omni-relation.backend.ts +21 -21
- package/src/omni-relation.dto.ts +142 -142
- package/src/omni-relation.service.ts +159 -159
- package/src/omni-scope.ts +161 -161
- package/src/omni-scoped.backend.ts +110 -110
- package/src/omni-scoped.repository.ts +34 -34
- package/src/omni-scoped.service.ts +338 -338
- package/src/omnikernel.diagnostics.ts +83 -83
- package/src/omnikernel.module.ts +97 -97
- package/src/omnikernel.query.service.ts +108 -108
|
@@ -1,294 +1,294 @@
|
|
|
1
|
-
import {
|
|
2
|
-
compileProjectionUniqueConstraintPredicate,
|
|
3
|
-
createProjectionSchemaOptions,
|
|
4
|
-
defineProjectionResource,
|
|
5
|
-
getProjectionReferenceColumnNames,
|
|
6
|
-
getProjectionReferenceIndexName,
|
|
7
|
-
getProjectionReferenceTargetColumnNames,
|
|
8
|
-
getProjectionUniqueConstraintColumnNames,
|
|
9
|
-
type ProjectionDialect,
|
|
10
|
-
type ProjectionResourceDefinition,
|
|
11
|
-
} from '@nest-yalc-2/crud-gen';
|
|
12
|
-
import {
|
|
13
|
-
Column,
|
|
14
|
-
Entity,
|
|
15
|
-
ForeignKey,
|
|
16
|
-
Index,
|
|
17
|
-
PrimaryColumn,
|
|
18
|
-
Table,
|
|
19
|
-
type EntityTarget,
|
|
20
|
-
type EntitySchemaColumnOptions,
|
|
21
|
-
type ObjectLiteral,
|
|
22
|
-
} from 'typeorm';
|
|
23
|
-
import { OmniRecordEntity } from './base/omni-record.entity.js';
|
|
24
|
-
import { OmniRecordStatus } from './omni-record-status.enum.js';
|
|
25
|
-
|
|
26
|
-
export interface OmniExtensionProjectionDefinition extends ProjectionResourceDefinition {
|
|
27
|
-
/**
|
|
28
|
-
* Immutable metadata written only to the Omni record that owns every
|
|
29
|
-
* extension row. The extension table never mirrors these values.
|
|
30
|
-
*/
|
|
31
|
-
owner: {
|
|
32
|
-
kind: string;
|
|
33
|
-
title: string;
|
|
34
|
-
status: OmniRecordStatus;
|
|
35
|
-
schema: {
|
|
36
|
-
id: string;
|
|
37
|
-
version: number;
|
|
38
|
-
};
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function assertNonEmptyString(
|
|
43
|
-
value: unknown,
|
|
44
|
-
label: string,
|
|
45
|
-
max: number,
|
|
46
|
-
): void {
|
|
47
|
-
if (
|
|
48
|
-
typeof value !== 'string' ||
|
|
49
|
-
value.trim().length === 0 ||
|
|
50
|
-
value.length > max
|
|
51
|
-
) {
|
|
52
|
-
throw new TypeError(
|
|
53
|
-
`${label} must be a non-empty string up to ${max} characters.`,
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Defines an owner-extension projection. The inherited projection metadata
|
|
60
|
-
* remains the single source for public fields, codecs, JSON paths, and query
|
|
61
|
-
* capabilities; the extra owner block fixes Omni record metadata server-side.
|
|
62
|
-
*/
|
|
63
|
-
export function defineOmniExtensionProjection<
|
|
64
|
-
TDefinition extends OmniExtensionProjectionDefinition,
|
|
65
|
-
>(definition: TDefinition): Readonly<TDefinition> {
|
|
66
|
-
assertNonEmptyString(definition.owner?.kind, 'Omni extension owner kind', 64);
|
|
67
|
-
assertNonEmptyString(
|
|
68
|
-
definition.owner?.title,
|
|
69
|
-
'Omni extension owner title',
|
|
70
|
-
255,
|
|
71
|
-
);
|
|
72
|
-
assertNonEmptyString(
|
|
73
|
-
definition.owner?.schema?.id,
|
|
74
|
-
'Omni extension owner schema id',
|
|
75
|
-
128,
|
|
76
|
-
);
|
|
77
|
-
if (
|
|
78
|
-
!Number.isInteger(definition.owner?.schema?.version) ||
|
|
79
|
-
definition.owner.schema.version < 1 ||
|
|
80
|
-
definition.owner.schema.version > 2_147_483_647
|
|
81
|
-
) {
|
|
82
|
-
throw new TypeError(
|
|
83
|
-
'Omni extension owner schema version must be a positive signed 32-bit integer.',
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
if (!Object.values(OmniRecordStatus).includes(definition.owner.status)) {
|
|
87
|
-
throw new TypeError(
|
|
88
|
-
'Omni extension owner status must be an Omni record status.',
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return defineProjectionResource(definition);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function extensionSchema(
|
|
96
|
-
definition: OmniExtensionProjectionDefinition,
|
|
97
|
-
dialect: ProjectionDialect,
|
|
98
|
-
): {
|
|
99
|
-
columns: Record<string, EntitySchemaColumnOptions>;
|
|
100
|
-
indices: ReturnType<typeof createProjectionSchemaOptions>['indices'];
|
|
101
|
-
foreignKeys: Array<{
|
|
102
|
-
name: string;
|
|
103
|
-
columnNames: string[];
|
|
104
|
-
referencedTableName: string;
|
|
105
|
-
referencedColumnNames: string[];
|
|
106
|
-
onDelete: 'RESTRICT' | 'NO ACTION';
|
|
107
|
-
}>;
|
|
108
|
-
} {
|
|
109
|
-
const projectionSchema = createProjectionSchemaOptions(definition, dialect);
|
|
110
|
-
const { [definition.revision.column]: _ownerRevision, ...columns } =
|
|
111
|
-
projectionSchema.columns;
|
|
112
|
-
const scopeColumn = definition.scope.column;
|
|
113
|
-
const identityColumn = definition.identity.column;
|
|
114
|
-
|
|
115
|
-
columns[scopeColumn] = {
|
|
116
|
-
...columns[scopeColumn],
|
|
117
|
-
primary: true,
|
|
118
|
-
nullable: false,
|
|
119
|
-
};
|
|
120
|
-
columns[identityColumn] = {
|
|
121
|
-
...columns[identityColumn],
|
|
122
|
-
primary: true,
|
|
123
|
-
nullable: false,
|
|
124
|
-
length: 36,
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const references = definition.references ?? [];
|
|
128
|
-
const uniqueConstraints = definition.uniqueConstraints ?? [];
|
|
129
|
-
return {
|
|
130
|
-
columns,
|
|
131
|
-
indices: [
|
|
132
|
-
...projectionSchema.indices,
|
|
133
|
-
...references.map((reference) => ({
|
|
134
|
-
name: getProjectionReferenceIndexName(reference),
|
|
135
|
-
columns: getProjectionReferenceColumnNames(definition, reference),
|
|
136
|
-
unique: false,
|
|
137
|
-
})),
|
|
138
|
-
...uniqueConstraints.map((constraint) => ({
|
|
139
|
-
name: constraint.name,
|
|
140
|
-
columns: getProjectionUniqueConstraintColumnNames(
|
|
141
|
-
definition,
|
|
142
|
-
constraint,
|
|
143
|
-
),
|
|
144
|
-
unique: true,
|
|
145
|
-
where: compileProjectionUniqueConstraintPredicate(
|
|
146
|
-
definition,
|
|
147
|
-
constraint,
|
|
148
|
-
dialect.name,
|
|
149
|
-
),
|
|
150
|
-
})),
|
|
151
|
-
],
|
|
152
|
-
foreignKeys: references.map((reference) => ({
|
|
153
|
-
name: reference.name,
|
|
154
|
-
columnNames: getProjectionReferenceColumnNames(definition, reference),
|
|
155
|
-
referencedTableName: reference.target.tableName,
|
|
156
|
-
referencedColumnNames: getProjectionReferenceTargetColumnNames(reference),
|
|
157
|
-
onDelete: reference.onDelete,
|
|
158
|
-
})),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function migrationColumnType(type: EntitySchemaColumnOptions['type']): string {
|
|
163
|
-
if (type === String) return 'varchar';
|
|
164
|
-
if (type === Number) return 'integer';
|
|
165
|
-
if (type === Boolean) return 'boolean';
|
|
166
|
-
if (type === 'simple-json') return 'text';
|
|
167
|
-
if (typeof type === 'string') return type;
|
|
168
|
-
throw new TypeError('Omni extension projection column type is unsupported.');
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Builds a TypeORM entity target for one extension table. This is deliberately
|
|
173
|
-
* a decorated class, rather than an EntitySchema: TypeORM builds EntitySchema
|
|
174
|
-
* metadata separately and cannot resolve an EntitySchema foreign key to the
|
|
175
|
-
* decorated OmniRecordEntity. The generated class keeps both sides in the
|
|
176
|
-
* same metadata pass, allowing the composite FK to be synchronized normally.
|
|
177
|
-
*/
|
|
178
|
-
export function createOmniExtensionProjectionEntity<
|
|
179
|
-
Extension extends ObjectLiteral,
|
|
180
|
-
>(
|
|
181
|
-
definition: OmniExtensionProjectionDefinition,
|
|
182
|
-
dialect: ProjectionDialect,
|
|
183
|
-
): EntityTarget<Extension> {
|
|
184
|
-
const { columns, indices, foreignKeys } = extensionSchema(
|
|
185
|
-
definition,
|
|
186
|
-
dialect,
|
|
187
|
-
);
|
|
188
|
-
const scopeColumn = definition.scope.column;
|
|
189
|
-
const identityColumn = definition.identity.column;
|
|
190
|
-
|
|
191
|
-
class OmniExtensionProjectionEntity {}
|
|
192
|
-
|
|
193
|
-
Entity(definition.tableName)(OmniExtensionProjectionEntity);
|
|
194
|
-
for (const [columnName, declared] of Object.entries(columns)) {
|
|
195
|
-
const options = { ...declared };
|
|
196
|
-
const primary = options.primary === true;
|
|
197
|
-
delete options.primary;
|
|
198
|
-
if (primary) {
|
|
199
|
-
PrimaryColumn(options.type as never, options as never)(
|
|
200
|
-
OmniExtensionProjectionEntity.prototype,
|
|
201
|
-
columnName,
|
|
202
|
-
);
|
|
203
|
-
} else if (typeof options.type === 'function') {
|
|
204
|
-
Column(options as never)(
|
|
205
|
-
OmniExtensionProjectionEntity.prototype,
|
|
206
|
-
columnName,
|
|
207
|
-
);
|
|
208
|
-
} else {
|
|
209
|
-
Column(options.type as never, options as never)(
|
|
210
|
-
OmniExtensionProjectionEntity.prototype,
|
|
211
|
-
columnName,
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
for (const index of indices) {
|
|
216
|
-
if (!index.name || !Array.isArray(index.columns)) {
|
|
217
|
-
throw new TypeError(
|
|
218
|
-
'Omni extension projection indexes require a name and column list.',
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
Index(index.name, index.columns, {
|
|
222
|
-
unique: index.unique,
|
|
223
|
-
...(index.where ? { where: index.where } : {}),
|
|
224
|
-
})(OmniExtensionProjectionEntity);
|
|
225
|
-
}
|
|
226
|
-
for (const foreignKey of foreignKeys) {
|
|
227
|
-
ForeignKey(
|
|
228
|
-
foreignKey.referencedTableName,
|
|
229
|
-
foreignKey.columnNames as never,
|
|
230
|
-
foreignKey.referencedColumnNames as never,
|
|
231
|
-
{ name: foreignKey.name, onDelete: foreignKey.onDelete },
|
|
232
|
-
)(OmniExtensionProjectionEntity);
|
|
233
|
-
}
|
|
234
|
-
ForeignKey(
|
|
235
|
-
() => OmniRecordEntity,
|
|
236
|
-
[scopeColumn, identityColumn],
|
|
237
|
-
['scopeId', 'guid'],
|
|
238
|
-
{ onDelete: 'CASCADE' },
|
|
239
|
-
)(OmniExtensionProjectionEntity);
|
|
240
|
-
|
|
241
|
-
return OmniExtensionProjectionEntity as unknown as EntityTarget<Extension>;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Creates reviewed-migration DDL for an extension table. Applications pass
|
|
246
|
-
* the returned Table to QueryRunner.createTable; runtime CRUD never creates
|
|
247
|
-
* schema objects. JSON storage remains a ProjectionDialect concern.
|
|
248
|
-
*/
|
|
249
|
-
export function createOmniExtensionProjectionTable(
|
|
250
|
-
definition: OmniExtensionProjectionDefinition,
|
|
251
|
-
dialect: ProjectionDialect,
|
|
252
|
-
): Table {
|
|
253
|
-
const { columns, indices, foreignKeys } = extensionSchema(
|
|
254
|
-
definition,
|
|
255
|
-
dialect,
|
|
256
|
-
);
|
|
257
|
-
const scopeColumn = definition.scope.column;
|
|
258
|
-
const identityColumn = definition.identity.column;
|
|
259
|
-
|
|
260
|
-
return new Table({
|
|
261
|
-
name: definition.tableName,
|
|
262
|
-
columns: Object.entries(columns).map(([name, options]) => ({
|
|
263
|
-
name,
|
|
264
|
-
type: migrationColumnType(options.type),
|
|
265
|
-
isPrimary: options.primary === true,
|
|
266
|
-
isNullable: options.nullable === true,
|
|
267
|
-
...(options.length !== undefined
|
|
268
|
-
? { length: String(options.length) }
|
|
269
|
-
: {}),
|
|
270
|
-
})),
|
|
271
|
-
indices: indices.map((index) => {
|
|
272
|
-
if (!index.name || !Array.isArray(index.columns)) {
|
|
273
|
-
throw new TypeError(
|
|
274
|
-
'Omni extension projection indexes require a name and column list.',
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
return {
|
|
278
|
-
name: index.name,
|
|
279
|
-
columnNames: index.columns,
|
|
280
|
-
isUnique: index.unique === true,
|
|
281
|
-
...(index.where ? { where: index.where } : {}),
|
|
282
|
-
};
|
|
283
|
-
}),
|
|
284
|
-
foreignKeys: [
|
|
285
|
-
{
|
|
286
|
-
columnNames: [scopeColumn, identityColumn],
|
|
287
|
-
referencedTableName: 'omni-record',
|
|
288
|
-
referencedColumnNames: ['scopeId', 'guid'],
|
|
289
|
-
onDelete: 'CASCADE',
|
|
290
|
-
},
|
|
291
|
-
...foreignKeys,
|
|
292
|
-
],
|
|
293
|
-
});
|
|
294
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
compileProjectionUniqueConstraintPredicate,
|
|
3
|
+
createProjectionSchemaOptions,
|
|
4
|
+
defineProjectionResource,
|
|
5
|
+
getProjectionReferenceColumnNames,
|
|
6
|
+
getProjectionReferenceIndexName,
|
|
7
|
+
getProjectionReferenceTargetColumnNames,
|
|
8
|
+
getProjectionUniqueConstraintColumnNames,
|
|
9
|
+
type ProjectionDialect,
|
|
10
|
+
type ProjectionResourceDefinition,
|
|
11
|
+
} from '@nest-yalc-2/crud-gen';
|
|
12
|
+
import {
|
|
13
|
+
Column,
|
|
14
|
+
Entity,
|
|
15
|
+
ForeignKey,
|
|
16
|
+
Index,
|
|
17
|
+
PrimaryColumn,
|
|
18
|
+
Table,
|
|
19
|
+
type EntityTarget,
|
|
20
|
+
type EntitySchemaColumnOptions,
|
|
21
|
+
type ObjectLiteral,
|
|
22
|
+
} from 'typeorm';
|
|
23
|
+
import { OmniRecordEntity } from './base/omni-record.entity.js';
|
|
24
|
+
import { OmniRecordStatus } from './omni-record-status.enum.js';
|
|
25
|
+
|
|
26
|
+
export interface OmniExtensionProjectionDefinition extends ProjectionResourceDefinition {
|
|
27
|
+
/**
|
|
28
|
+
* Immutable metadata written only to the Omni record that owns every
|
|
29
|
+
* extension row. The extension table never mirrors these values.
|
|
30
|
+
*/
|
|
31
|
+
owner: {
|
|
32
|
+
kind: string;
|
|
33
|
+
title: string;
|
|
34
|
+
status: OmniRecordStatus;
|
|
35
|
+
schema: {
|
|
36
|
+
id: string;
|
|
37
|
+
version: number;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assertNonEmptyString(
|
|
43
|
+
value: unknown,
|
|
44
|
+
label: string,
|
|
45
|
+
max: number,
|
|
46
|
+
): void {
|
|
47
|
+
if (
|
|
48
|
+
typeof value !== 'string' ||
|
|
49
|
+
value.trim().length === 0 ||
|
|
50
|
+
value.length > max
|
|
51
|
+
) {
|
|
52
|
+
throw new TypeError(
|
|
53
|
+
`${label} must be a non-empty string up to ${max} characters.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Defines an owner-extension projection. The inherited projection metadata
|
|
60
|
+
* remains the single source for public fields, codecs, JSON paths, and query
|
|
61
|
+
* capabilities; the extra owner block fixes Omni record metadata server-side.
|
|
62
|
+
*/
|
|
63
|
+
export function defineOmniExtensionProjection<
|
|
64
|
+
TDefinition extends OmniExtensionProjectionDefinition,
|
|
65
|
+
>(definition: TDefinition): Readonly<TDefinition> {
|
|
66
|
+
assertNonEmptyString(definition.owner?.kind, 'Omni extension owner kind', 64);
|
|
67
|
+
assertNonEmptyString(
|
|
68
|
+
definition.owner?.title,
|
|
69
|
+
'Omni extension owner title',
|
|
70
|
+
255,
|
|
71
|
+
);
|
|
72
|
+
assertNonEmptyString(
|
|
73
|
+
definition.owner?.schema?.id,
|
|
74
|
+
'Omni extension owner schema id',
|
|
75
|
+
128,
|
|
76
|
+
);
|
|
77
|
+
if (
|
|
78
|
+
!Number.isInteger(definition.owner?.schema?.version) ||
|
|
79
|
+
definition.owner.schema.version < 1 ||
|
|
80
|
+
definition.owner.schema.version > 2_147_483_647
|
|
81
|
+
) {
|
|
82
|
+
throw new TypeError(
|
|
83
|
+
'Omni extension owner schema version must be a positive signed 32-bit integer.',
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (!Object.values(OmniRecordStatus).includes(definition.owner.status)) {
|
|
87
|
+
throw new TypeError(
|
|
88
|
+
'Omni extension owner status must be an Omni record status.',
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return defineProjectionResource(definition);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function extensionSchema(
|
|
96
|
+
definition: OmniExtensionProjectionDefinition,
|
|
97
|
+
dialect: ProjectionDialect,
|
|
98
|
+
): {
|
|
99
|
+
columns: Record<string, EntitySchemaColumnOptions>;
|
|
100
|
+
indices: ReturnType<typeof createProjectionSchemaOptions>['indices'];
|
|
101
|
+
foreignKeys: Array<{
|
|
102
|
+
name: string;
|
|
103
|
+
columnNames: string[];
|
|
104
|
+
referencedTableName: string;
|
|
105
|
+
referencedColumnNames: string[];
|
|
106
|
+
onDelete: 'RESTRICT' | 'NO ACTION';
|
|
107
|
+
}>;
|
|
108
|
+
} {
|
|
109
|
+
const projectionSchema = createProjectionSchemaOptions(definition, dialect);
|
|
110
|
+
const { [definition.revision.column]: _ownerRevision, ...columns } =
|
|
111
|
+
projectionSchema.columns;
|
|
112
|
+
const scopeColumn = definition.scope.column;
|
|
113
|
+
const identityColumn = definition.identity.column;
|
|
114
|
+
|
|
115
|
+
columns[scopeColumn] = {
|
|
116
|
+
...columns[scopeColumn],
|
|
117
|
+
primary: true,
|
|
118
|
+
nullable: false,
|
|
119
|
+
};
|
|
120
|
+
columns[identityColumn] = {
|
|
121
|
+
...columns[identityColumn],
|
|
122
|
+
primary: true,
|
|
123
|
+
nullable: false,
|
|
124
|
+
length: 36,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const references = definition.references ?? [];
|
|
128
|
+
const uniqueConstraints = definition.uniqueConstraints ?? [];
|
|
129
|
+
return {
|
|
130
|
+
columns,
|
|
131
|
+
indices: [
|
|
132
|
+
...projectionSchema.indices,
|
|
133
|
+
...references.map((reference) => ({
|
|
134
|
+
name: getProjectionReferenceIndexName(reference),
|
|
135
|
+
columns: getProjectionReferenceColumnNames(definition, reference),
|
|
136
|
+
unique: false,
|
|
137
|
+
})),
|
|
138
|
+
...uniqueConstraints.map((constraint) => ({
|
|
139
|
+
name: constraint.name,
|
|
140
|
+
columns: getProjectionUniqueConstraintColumnNames(
|
|
141
|
+
definition,
|
|
142
|
+
constraint,
|
|
143
|
+
),
|
|
144
|
+
unique: true,
|
|
145
|
+
where: compileProjectionUniqueConstraintPredicate(
|
|
146
|
+
definition,
|
|
147
|
+
constraint,
|
|
148
|
+
dialect.name,
|
|
149
|
+
),
|
|
150
|
+
})),
|
|
151
|
+
],
|
|
152
|
+
foreignKeys: references.map((reference) => ({
|
|
153
|
+
name: reference.name,
|
|
154
|
+
columnNames: getProjectionReferenceColumnNames(definition, reference),
|
|
155
|
+
referencedTableName: reference.target.tableName,
|
|
156
|
+
referencedColumnNames: getProjectionReferenceTargetColumnNames(reference),
|
|
157
|
+
onDelete: reference.onDelete,
|
|
158
|
+
})),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function migrationColumnType(type: EntitySchemaColumnOptions['type']): string {
|
|
163
|
+
if (type === String) return 'varchar';
|
|
164
|
+
if (type === Number) return 'integer';
|
|
165
|
+
if (type === Boolean) return 'boolean';
|
|
166
|
+
if (type === 'simple-json') return 'text';
|
|
167
|
+
if (typeof type === 'string') return type;
|
|
168
|
+
throw new TypeError('Omni extension projection column type is unsupported.');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Builds a TypeORM entity target for one extension table. This is deliberately
|
|
173
|
+
* a decorated class, rather than an EntitySchema: TypeORM builds EntitySchema
|
|
174
|
+
* metadata separately and cannot resolve an EntitySchema foreign key to the
|
|
175
|
+
* decorated OmniRecordEntity. The generated class keeps both sides in the
|
|
176
|
+
* same metadata pass, allowing the composite FK to be synchronized normally.
|
|
177
|
+
*/
|
|
178
|
+
export function createOmniExtensionProjectionEntity<
|
|
179
|
+
Extension extends ObjectLiteral,
|
|
180
|
+
>(
|
|
181
|
+
definition: OmniExtensionProjectionDefinition,
|
|
182
|
+
dialect: ProjectionDialect,
|
|
183
|
+
): EntityTarget<Extension> {
|
|
184
|
+
const { columns, indices, foreignKeys } = extensionSchema(
|
|
185
|
+
definition,
|
|
186
|
+
dialect,
|
|
187
|
+
);
|
|
188
|
+
const scopeColumn = definition.scope.column;
|
|
189
|
+
const identityColumn = definition.identity.column;
|
|
190
|
+
|
|
191
|
+
class OmniExtensionProjectionEntity {}
|
|
192
|
+
|
|
193
|
+
Entity(definition.tableName)(OmniExtensionProjectionEntity);
|
|
194
|
+
for (const [columnName, declared] of Object.entries(columns)) {
|
|
195
|
+
const options = { ...declared };
|
|
196
|
+
const primary = options.primary === true;
|
|
197
|
+
delete options.primary;
|
|
198
|
+
if (primary) {
|
|
199
|
+
PrimaryColumn(options.type as never, options as never)(
|
|
200
|
+
OmniExtensionProjectionEntity.prototype,
|
|
201
|
+
columnName,
|
|
202
|
+
);
|
|
203
|
+
} else if (typeof options.type === 'function') {
|
|
204
|
+
Column(options as never)(
|
|
205
|
+
OmniExtensionProjectionEntity.prototype,
|
|
206
|
+
columnName,
|
|
207
|
+
);
|
|
208
|
+
} else {
|
|
209
|
+
Column(options.type as never, options as never)(
|
|
210
|
+
OmniExtensionProjectionEntity.prototype,
|
|
211
|
+
columnName,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (const index of indices) {
|
|
216
|
+
if (!index.name || !Array.isArray(index.columns)) {
|
|
217
|
+
throw new TypeError(
|
|
218
|
+
'Omni extension projection indexes require a name and column list.',
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
Index(index.name, index.columns, {
|
|
222
|
+
unique: index.unique,
|
|
223
|
+
...(index.where ? { where: index.where } : {}),
|
|
224
|
+
})(OmniExtensionProjectionEntity);
|
|
225
|
+
}
|
|
226
|
+
for (const foreignKey of foreignKeys) {
|
|
227
|
+
ForeignKey(
|
|
228
|
+
foreignKey.referencedTableName,
|
|
229
|
+
foreignKey.columnNames as never,
|
|
230
|
+
foreignKey.referencedColumnNames as never,
|
|
231
|
+
{ name: foreignKey.name, onDelete: foreignKey.onDelete },
|
|
232
|
+
)(OmniExtensionProjectionEntity);
|
|
233
|
+
}
|
|
234
|
+
ForeignKey(
|
|
235
|
+
() => OmniRecordEntity,
|
|
236
|
+
[scopeColumn, identityColumn],
|
|
237
|
+
['scopeId', 'guid'],
|
|
238
|
+
{ onDelete: 'CASCADE' },
|
|
239
|
+
)(OmniExtensionProjectionEntity);
|
|
240
|
+
|
|
241
|
+
return OmniExtensionProjectionEntity as unknown as EntityTarget<Extension>;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Creates reviewed-migration DDL for an extension table. Applications pass
|
|
246
|
+
* the returned Table to QueryRunner.createTable; runtime CRUD never creates
|
|
247
|
+
* schema objects. JSON storage remains a ProjectionDialect concern.
|
|
248
|
+
*/
|
|
249
|
+
export function createOmniExtensionProjectionTable(
|
|
250
|
+
definition: OmniExtensionProjectionDefinition,
|
|
251
|
+
dialect: ProjectionDialect,
|
|
252
|
+
): Table {
|
|
253
|
+
const { columns, indices, foreignKeys } = extensionSchema(
|
|
254
|
+
definition,
|
|
255
|
+
dialect,
|
|
256
|
+
);
|
|
257
|
+
const scopeColumn = definition.scope.column;
|
|
258
|
+
const identityColumn = definition.identity.column;
|
|
259
|
+
|
|
260
|
+
return new Table({
|
|
261
|
+
name: definition.tableName,
|
|
262
|
+
columns: Object.entries(columns).map(([name, options]) => ({
|
|
263
|
+
name,
|
|
264
|
+
type: migrationColumnType(options.type),
|
|
265
|
+
isPrimary: options.primary === true,
|
|
266
|
+
isNullable: options.nullable === true,
|
|
267
|
+
...(options.length !== undefined
|
|
268
|
+
? { length: String(options.length) }
|
|
269
|
+
: {}),
|
|
270
|
+
})),
|
|
271
|
+
indices: indices.map((index) => {
|
|
272
|
+
if (!index.name || !Array.isArray(index.columns)) {
|
|
273
|
+
throw new TypeError(
|
|
274
|
+
'Omni extension projection indexes require a name and column list.',
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
name: index.name,
|
|
279
|
+
columnNames: index.columns,
|
|
280
|
+
isUnique: index.unique === true,
|
|
281
|
+
...(index.where ? { where: index.where } : {}),
|
|
282
|
+
};
|
|
283
|
+
}),
|
|
284
|
+
foreignKeys: [
|
|
285
|
+
{
|
|
286
|
+
columnNames: [scopeColumn, identityColumn],
|
|
287
|
+
referencedTableName: 'omni-record',
|
|
288
|
+
referencedColumnNames: ['scopeId', 'guid'],
|
|
289
|
+
onDelete: 'CASCADE',
|
|
290
|
+
},
|
|
291
|
+
...foreignKeys,
|
|
292
|
+
],
|
|
293
|
+
});
|
|
294
|
+
}
|