@lossless.org/client 1.1.0 → 1.2.0
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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/nosqldb/classes.collection.js +84 -20
- package/dist_ts/nosqldb/classes.collectiontopology.d.ts +11 -0
- package/dist_ts/nosqldb/classes.collectiontopology.js +16 -1
- package/dist_ts/nosqldb/classes.doc.d.ts +8 -0
- package/dist_ts/nosqldb/classes.doc.js +27 -3
- package/dist_ts/nosqldb/classes.exactpersistence.js +10 -2
- package/dist_ts/nosqldb/classes.persistence.d.ts +6 -0
- package/dist_ts/nosqldb/classes.persistence.js +13 -1
- package/package.json +1 -1
- package/readme.md +1 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/nosqldb/classes.collection.ts +132 -27
- package/ts/nosqldb/classes.collectiontopology.ts +25 -0
- package/ts/nosqldb/classes.doc.ts +50 -2
- package/ts/nosqldb/classes.exactpersistence.ts +9 -1
- package/ts/nosqldb/classes.persistence.ts +17 -0
|
@@ -298,6 +298,31 @@ export const getExpectedCollectionTopologyForSchema = (
|
|
|
298
298
|
});
|
|
299
299
|
};
|
|
300
300
|
|
|
301
|
+
/**
|
|
302
|
+
* @internal A single-field ascending unique index is what makes a declared
|
|
303
|
+
* identity globally constraining, whatever the index is called.
|
|
304
|
+
*/
|
|
305
|
+
export const isIdentityIndexFor = (
|
|
306
|
+
indexArg: INormalizedCollectionModelSchema['indexes'][number],
|
|
307
|
+
fieldArg: string,
|
|
308
|
+
): boolean =>
|
|
309
|
+
indexArg.options.unique === true &&
|
|
310
|
+
indexArg.key.length === 1 &&
|
|
311
|
+
indexArg.key[0][0] === fieldArg &&
|
|
312
|
+
indexArg.key[0][1] === 1;
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* @internal Name of the unique index that backs an identity field in a bound
|
|
316
|
+
* schema. Index-creation fallbacks resolve it here so that a model naming its
|
|
317
|
+
* own backing index never gets a second unique index over the same key.
|
|
318
|
+
*/
|
|
319
|
+
export const getIdentityIndexName = (
|
|
320
|
+
schemaArg: INormalizedCollectionModelSchema | undefined,
|
|
321
|
+
fieldArg: string,
|
|
322
|
+
): string | undefined =>
|
|
323
|
+
schemaArg?.indexes.find((indexArg) => isIdentityIndexFor(indexArg, fieldArg))
|
|
324
|
+
?.name;
|
|
325
|
+
|
|
301
326
|
const normalizeActualIndex = (
|
|
302
327
|
rawIndexArg: unknown,
|
|
303
328
|
): IActualIndexNormalization => {
|
|
@@ -78,6 +78,7 @@ interface ISmartdataDecoratorMetadata extends DecoratorMetadataObject {
|
|
|
78
78
|
saveableProperties?: string[];
|
|
79
79
|
uniqueIndexes?: string[];
|
|
80
80
|
identityValueTypes?: Record<string, TSmartdataIdentityValueType>;
|
|
81
|
+
identityIndexNames?: Record<string, string>;
|
|
81
82
|
regularIndexes?: Array<{field: string, options: IIndexOptions}>;
|
|
82
83
|
compoundIndexes?: Array<{
|
|
83
84
|
name: string;
|
|
@@ -98,6 +99,14 @@ export interface IUnIOptions {
|
|
|
98
99
|
* non-empty string. Numeric identities require an explicit opt-in.
|
|
99
100
|
*/
|
|
100
101
|
valueType?: TSmartdataIdentityValueType;
|
|
102
|
+
/**
|
|
103
|
+
* Name of the single-field ascending unique index that backs this identity.
|
|
104
|
+
* Defaults to `<field>_1`. A collection whose unique index was created by a
|
|
105
|
+
* migration under another name declares that name here: MongoDB refuses a
|
|
106
|
+
* second index over the same key, so without it the identity could not be
|
|
107
|
+
* declared at all.
|
|
108
|
+
*/
|
|
109
|
+
indexName?: string;
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
/**
|
|
@@ -323,11 +332,13 @@ export function unI(optionsArg: IUnIOptions = {}) {
|
|
|
323
332
|
typeof optionsArg !== 'object'
|
|
324
333
|
|| optionsArg === null
|
|
325
334
|
|| Array.isArray(optionsArg)
|
|
326
|
-
|| Object.keys(optionsArg).some(
|
|
335
|
+
|| Object.keys(optionsArg).some(
|
|
336
|
+
(keyArg) => keyArg !== 'valueType' && keyArg !== 'indexName',
|
|
337
|
+
)
|
|
327
338
|
) {
|
|
328
339
|
throw new SmartdataPersistenceError(
|
|
329
340
|
'invalid_configuration',
|
|
330
|
-
'unI options must be an object containing only valueType.',
|
|
341
|
+
'unI options must be an object containing only valueType and indexName.',
|
|
331
342
|
);
|
|
332
343
|
}
|
|
333
344
|
const valueType = optionsArg.valueType ?? 'string';
|
|
@@ -337,6 +348,25 @@ export function unI(optionsArg: IUnIOptions = {}) {
|
|
|
337
348
|
'unI valueType must be "string" or "positiveSafeInteger".',
|
|
338
349
|
);
|
|
339
350
|
}
|
|
351
|
+
const indexName = optionsArg.indexName;
|
|
352
|
+
if (indexName !== undefined) {
|
|
353
|
+
if (
|
|
354
|
+
typeof indexName !== 'string'
|
|
355
|
+
|| indexName.trim().length === 0
|
|
356
|
+
|| indexName.includes('\0')
|
|
357
|
+
) {
|
|
358
|
+
throw new SmartdataPersistenceError(
|
|
359
|
+
'invalid_configuration',
|
|
360
|
+
'unI indexName must be a non-empty index name.',
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
if (indexName === '_id_') {
|
|
364
|
+
throw new SmartdataPersistenceError(
|
|
365
|
+
'invalid_configuration',
|
|
366
|
+
'unI indexName "_id_" is reserved for MongoDB.',
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
340
370
|
return (value: undefined, context: ClassFieldDecoratorContext) => {
|
|
341
371
|
if (context.kind !== 'field') {
|
|
342
372
|
throw new Error('unI can only decorate fields');
|
|
@@ -365,6 +395,24 @@ export function unI(optionsArg: IUnIOptions = {}) {
|
|
|
365
395
|
);
|
|
366
396
|
}
|
|
367
397
|
metadata.identityValueTypes![propName] = valueType;
|
|
398
|
+
// An unnamed declaration writes no entry, so a subclass that redeclares
|
|
399
|
+
// the identity without a name keeps the index the base class named. Only
|
|
400
|
+
// a second, different name is a contradiction worth refusing.
|
|
401
|
+
if (indexName !== undefined) {
|
|
402
|
+
if (
|
|
403
|
+
!Object.prototype.hasOwnProperty.call(metadata, 'identityIndexNames')
|
|
404
|
+
) {
|
|
405
|
+
metadata.identityIndexNames = { ...(metadata.identityIndexNames || {}) };
|
|
406
|
+
}
|
|
407
|
+
const existingIndexName = metadata.identityIndexNames![propName];
|
|
408
|
+
if (existingIndexName && existingIndexName !== indexName) {
|
|
409
|
+
throw new SmartdataPersistenceError(
|
|
410
|
+
'invalid_configuration',
|
|
411
|
+
`Identity field "${propName}" has divergent indexName declarations.`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
metadata.identityIndexNames![propName] = indexName;
|
|
415
|
+
}
|
|
368
416
|
|
|
369
417
|
// Also mark as saveable
|
|
370
418
|
if (!Object.prototype.hasOwnProperty.call(metadata, 'saveableProperties')) {
|
|
@@ -4,6 +4,7 @@ import { assertBsonWithoutMutation } from './classes.bsonassertion.js';
|
|
|
4
4
|
import { getOrdinaryPersistencePolicy } from './classes.ordinarypersistence.js';
|
|
5
5
|
|
|
6
6
|
import type { SmartdataCollection } from './classes.collection.js';
|
|
7
|
+
import { getIdentityIndexName } from './classes.collectiontopology.js';
|
|
7
8
|
import { registerCollectionReconnectHandler } from './classes.collectionlifecycle.js';
|
|
8
9
|
import type { SmartdataSession } from './classes.session.js';
|
|
9
10
|
import { acquireSmartdataSession } from './classes.session.js';
|
|
@@ -1024,10 +1025,17 @@ export class SmartdataExactCollection<
|
|
|
1024
1025
|
const missingUniqueIndexes = this.uniqueIndexes.filter(
|
|
1025
1026
|
(uniqueIndexArg) => !this.collection.uniqueIndexes.includes(uniqueIndexArg),
|
|
1026
1027
|
);
|
|
1028
|
+
const boundSchema = this.collection.getBoundModelSchema();
|
|
1027
1029
|
for (const uniqueIndex of missingUniqueIndexes) {
|
|
1028
1030
|
await targetCollectionArg.createIndex(
|
|
1029
1031
|
{ [uniqueIndex]: 1 },
|
|
1030
|
-
{
|
|
1032
|
+
{
|
|
1033
|
+
unique: true,
|
|
1034
|
+
// A model may back its identity with a differently named index;
|
|
1035
|
+
// creating `<field>_1` would duplicate that key.
|
|
1036
|
+
name: getIdentityIndexName(boundSchema, uniqueIndex)
|
|
1037
|
+
?? `${uniqueIndex}_1`,
|
|
1038
|
+
},
|
|
1031
1039
|
);
|
|
1032
1040
|
}
|
|
1033
1041
|
|
|
@@ -43,6 +43,23 @@ export const isMongoDuplicateKeyError = (
|
|
|
43
43
|
);
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* MongoDB refuses an index whose key or name collides with an existing one:
|
|
48
|
+
* `IndexOptionsConflict` when an equivalent index already carries another
|
|
49
|
+
* name, `IndexKeySpecsConflict` when the name is taken by another key.
|
|
50
|
+
*/
|
|
51
|
+
export const isMongoIndexConflictError = (
|
|
52
|
+
errorArg: unknown,
|
|
53
|
+
): errorArg is plugins.mongodb.MongoServerError => {
|
|
54
|
+
return (
|
|
55
|
+
errorArg instanceof plugins.mongodb.MongoServerError &&
|
|
56
|
+
(errorArg.code === 85 ||
|
|
57
|
+
errorArg.code === 86 ||
|
|
58
|
+
errorArg.codeName === 'IndexOptionsConflict' ||
|
|
59
|
+
errorArg.codeName === 'IndexKeySpecsConflict')
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
46
63
|
export const normalizeOrdinaryPersistenceError = (
|
|
47
64
|
errorArg: unknown,
|
|
48
65
|
messageArg: string,
|