@zakodium/adonis-mongodb 0.10.4 → 0.13.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/README.md +1 -1
- package/lib/adonis-typings/migration.d.ts +3 -2
- package/lib/adonis-typings/odm.d.ts +68 -2
- package/lib/commands/MongodbListMigrations.js +2 -2
- package/lib/commands/MongodbMakeMigration.js +2 -2
- package/lib/commands/MongodbMigrate.js +2 -2
- package/lib/commands/util/MigrationCommand.js +6 -6
- package/lib/commands/util/transformMigrations.js +3 -3
- package/lib/providers/MongodbProvider.js +2 -2
- package/lib/src/Auth/MongodbModelAuthProvider.d.ts +2 -2
- package/lib/src/Auth/MongodbModelAuthProvider.js +3 -4
- package/lib/src/Migration.js +35 -9
- package/lib/src/Model/Model.d.ts +17 -7
- package/lib/src/Model/Model.js +135 -61
- package/lib/src/Model/proxyHandler.d.ts +1 -4
- package/lib/src/Model/proxyHandler.js +6 -3
- package/package.json +15 -15
- package/src/Auth/MongodbModelAuthProvider.ts +3 -4
- package/src/Migration.ts +54 -10
- package/src/Model/Model.ts +177 -65
- package/src/Model/proxyHandler.ts +10 -5
package/src/Migration.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
ClientSession,
|
|
5
5
|
Db,
|
|
6
6
|
IndexSpecification,
|
|
7
|
+
DropIndexesOptions,
|
|
7
8
|
} from 'mongodb';
|
|
8
9
|
|
|
9
10
|
import type {
|
|
@@ -13,18 +14,26 @@ import type {
|
|
|
13
14
|
|
|
14
15
|
enum MigrationType {
|
|
15
16
|
CreateCollection = 'CreateCollection',
|
|
17
|
+
DropIndex = 'DropIndex',
|
|
16
18
|
CreateIndex = 'CreateIndex',
|
|
17
19
|
Custom = 'Custom',
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
interface CreateCollectionOperation {
|
|
21
23
|
type: MigrationType.CreateCollection;
|
|
22
|
-
|
|
24
|
+
collectionName: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface DropIndexOperation {
|
|
28
|
+
type: MigrationType.DropIndex;
|
|
29
|
+
collectionName: string;
|
|
30
|
+
indexName: string;
|
|
31
|
+
options?: DropIndexesOptions;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
interface CreateIndexOperation {
|
|
26
35
|
type: MigrationType.CreateIndex;
|
|
27
|
-
|
|
36
|
+
collectionName: string;
|
|
28
37
|
index: IndexSpecification;
|
|
29
38
|
options?: CreateIndexesOptions;
|
|
30
39
|
}
|
|
@@ -36,6 +45,7 @@ interface CustomOperation {
|
|
|
36
45
|
|
|
37
46
|
type MigrationOperation =
|
|
38
47
|
| CreateCollectionOperation
|
|
48
|
+
| DropIndexOperation
|
|
39
49
|
| CreateIndexOperation
|
|
40
50
|
| CustomOperation;
|
|
41
51
|
|
|
@@ -67,7 +77,20 @@ export default function createMigration(Database: DatabaseContract): any {
|
|
|
67
77
|
public createCollection(collectionName: string): void {
|
|
68
78
|
this.$operations.push({
|
|
69
79
|
type: MigrationType.CreateCollection,
|
|
70
|
-
|
|
80
|
+
collectionName,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public dropIndex(
|
|
85
|
+
collectionName: string,
|
|
86
|
+
indexName: string,
|
|
87
|
+
options?: DropIndexesOptions,
|
|
88
|
+
): void {
|
|
89
|
+
this.$operations.push({
|
|
90
|
+
type: MigrationType.DropIndex,
|
|
91
|
+
collectionName,
|
|
92
|
+
indexName,
|
|
93
|
+
options,
|
|
71
94
|
});
|
|
72
95
|
}
|
|
73
96
|
|
|
@@ -78,7 +101,7 @@ export default function createMigration(Database: DatabaseContract): any {
|
|
|
78
101
|
): void {
|
|
79
102
|
this.$operations.push({
|
|
80
103
|
type: MigrationType.CreateIndex,
|
|
81
|
-
|
|
104
|
+
collectionName,
|
|
82
105
|
index,
|
|
83
106
|
options,
|
|
84
107
|
});
|
|
@@ -94,6 +117,7 @@ export default function createMigration(Database: DatabaseContract): any {
|
|
|
94
117
|
public async execUp(): Promise<void> {
|
|
95
118
|
this.up();
|
|
96
119
|
await this._createCollections();
|
|
120
|
+
await this._dropIndexes();
|
|
97
121
|
await this._createIndexes();
|
|
98
122
|
await this._executeDeferred();
|
|
99
123
|
}
|
|
@@ -113,8 +137,10 @@ export default function createMigration(Database: DatabaseContract): any {
|
|
|
113
137
|
private async _createCollections(): Promise<void> {
|
|
114
138
|
const db = await this.$connection.database();
|
|
115
139
|
for (const op of this.$operations.filter(isCreateCollection)) {
|
|
116
|
-
this.$logger.info(`Creating collection ${op.
|
|
117
|
-
await db.createCollection(op.
|
|
140
|
+
this.$logger.info(`Creating collection ${op.collectionName}`);
|
|
141
|
+
await db.createCollection(op.collectionName, {
|
|
142
|
+
session: this.$session,
|
|
143
|
+
});
|
|
118
144
|
}
|
|
119
145
|
}
|
|
120
146
|
|
|
@@ -125,15 +151,29 @@ export default function createMigration(Database: DatabaseContract): any {
|
|
|
125
151
|
}
|
|
126
152
|
}
|
|
127
153
|
|
|
154
|
+
private async _dropIndexes(): Promise<void> {
|
|
155
|
+
const db = await this.$connection.database();
|
|
156
|
+
for (const op of this.$operations.filter(isDropIndex)) {
|
|
157
|
+
this.$logger.info(
|
|
158
|
+
`Dropping index ${op.indexName} on ${op.collectionName}`,
|
|
159
|
+
);
|
|
160
|
+
const collection = db.collection(op.collectionName);
|
|
161
|
+
// Index deletion cannot be done in a transaction.
|
|
162
|
+
await collection.dropIndex(op.indexName, { ...op.options });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
128
166
|
private async _createIndexes(): Promise<void> {
|
|
129
167
|
const db = await this.$connection.database();
|
|
130
168
|
const collections = await this._listCollections();
|
|
131
169
|
for (const op of this.$operations.filter(isCreateIndex)) {
|
|
132
|
-
this.$logger.info(`Creating index on ${op.
|
|
133
|
-
await db.createIndex(op.
|
|
170
|
+
this.$logger.info(`Creating index on ${op.collectionName}`);
|
|
171
|
+
await db.createIndex(op.collectionName, op.index, {
|
|
134
172
|
...op.options,
|
|
135
|
-
//
|
|
136
|
-
session: collections.includes(op.
|
|
173
|
+
// Index creation will fail if collection pre-exists the transaction.
|
|
174
|
+
session: collections.includes(op.collectionName)
|
|
175
|
+
? undefined
|
|
176
|
+
: this.$session,
|
|
137
177
|
});
|
|
138
178
|
}
|
|
139
179
|
}
|
|
@@ -154,6 +194,10 @@ function isCreateIndex(op: MigrationOperation): op is CreateIndexOperation {
|
|
|
154
194
|
return op.type === MigrationType.CreateIndex;
|
|
155
195
|
}
|
|
156
196
|
|
|
197
|
+
function isDropIndex(op: MigrationOperation): op is DropIndexOperation {
|
|
198
|
+
return op.type === MigrationType.DropIndex;
|
|
199
|
+
}
|
|
200
|
+
|
|
157
201
|
function isCustom(op: MigrationOperation): op is CustomOperation {
|
|
158
202
|
return op.type === MigrationType.Custom;
|
|
159
203
|
}
|
package/src/Model/Model.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
|
|
1
3
|
import { defineStaticProperty, Exception } from '@poppinss/utils';
|
|
2
4
|
import { cloneDeep, isEqual, pickBy, snakeCase } from 'lodash';
|
|
3
5
|
import {
|
|
@@ -8,9 +10,13 @@ import {
|
|
|
8
10
|
CountOptions,
|
|
9
11
|
DeleteOptions,
|
|
10
12
|
DistinctOptions,
|
|
13
|
+
Document,
|
|
14
|
+
ExplainVerbosityLike,
|
|
11
15
|
Filter,
|
|
12
16
|
FindOptions,
|
|
13
17
|
InsertOneOptions,
|
|
18
|
+
SortDirection,
|
|
19
|
+
WithId,
|
|
14
20
|
} from 'mongodb';
|
|
15
21
|
import pluralize from 'pluralize';
|
|
16
22
|
|
|
@@ -23,6 +29,8 @@ import {
|
|
|
23
29
|
ModelAdapterOptions,
|
|
24
30
|
ModelDocumentOptions,
|
|
25
31
|
FieldOptions,
|
|
32
|
+
QuerySortObject,
|
|
33
|
+
ForbiddenQueryOptions,
|
|
26
34
|
} from '@ioc:Zakodium/Mongodb/Odm';
|
|
27
35
|
|
|
28
36
|
import { proxyHandler } from './proxyHandler';
|
|
@@ -37,32 +45,97 @@ function mergeDriverOptions<
|
|
|
37
45
|
} as DriverOptionType;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
};
|
|
48
|
+
interface QueryLocalOptions {
|
|
49
|
+
sort: QuerySortObject;
|
|
50
|
+
skip?: number;
|
|
51
|
+
limit?: number;
|
|
45
52
|
}
|
|
46
53
|
|
|
54
|
+
const forbiddenQueryOptions: ForbiddenQueryOptions[] = [
|
|
55
|
+
'sort',
|
|
56
|
+
'skip',
|
|
57
|
+
'limit',
|
|
58
|
+
'explain',
|
|
59
|
+
];
|
|
60
|
+
|
|
47
61
|
class Query<ModelType extends typeof BaseModel>
|
|
48
62
|
implements QueryContract<InstanceType<ModelType>>
|
|
49
63
|
{
|
|
64
|
+
private localCustomSort = false;
|
|
65
|
+
private localOptions: QueryLocalOptions = {
|
|
66
|
+
sort: {
|
|
67
|
+
_id: 'descending',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
private getDriverOptions(): FindOptions<
|
|
72
|
+
ModelAttributes<InstanceType<ModelType>>
|
|
73
|
+
> {
|
|
74
|
+
return { ...mergeDriverOptions(this.options), ...this.localOptions };
|
|
75
|
+
}
|
|
76
|
+
|
|
50
77
|
public constructor(
|
|
51
78
|
private filter: Filter<ModelAttributes<InstanceType<ModelType>>>,
|
|
52
79
|
private options:
|
|
53
80
|
| ModelAdapterOptions<
|
|
54
|
-
|
|
81
|
+
Omit<
|
|
82
|
+
FindOptions<ModelAttributes<InstanceType<ModelType>>>,
|
|
83
|
+
ForbiddenQueryOptions
|
|
84
|
+
>
|
|
55
85
|
>
|
|
56
86
|
| undefined,
|
|
57
87
|
private modelConstructor: ModelType,
|
|
58
|
-
) {
|
|
88
|
+
) {
|
|
89
|
+
if (options?.driverOptions) {
|
|
90
|
+
for (const key of forbiddenQueryOptions) {
|
|
91
|
+
if (key in options.driverOptions) {
|
|
92
|
+
throw new TypeError(`${key} is not allowed in query's driverOptions`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public sort(sort: QuerySortObject): this {
|
|
99
|
+
if (!this.localCustomSort) {
|
|
100
|
+
this.localCustomSort = true;
|
|
101
|
+
this.localOptions.sort = sort;
|
|
102
|
+
} else {
|
|
103
|
+
Object.assign(this.localOptions.sort, sort);
|
|
104
|
+
}
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public sortBy(field: string, direction: SortDirection = 'ascending'): this {
|
|
109
|
+
return this.sort({ [field]: direction });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public skip(skip: number): this {
|
|
113
|
+
if (!Number.isInteger(skip)) {
|
|
114
|
+
throw new TypeError(`skip must be an integer`);
|
|
115
|
+
}
|
|
116
|
+
if (skip < 0) {
|
|
117
|
+
throw new TypeError(`skip must be at least zero`);
|
|
118
|
+
}
|
|
119
|
+
this.localOptions.skip = skip;
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public limit(limit: number): this {
|
|
124
|
+
if (!Number.isInteger(limit)) {
|
|
125
|
+
throw new TypeError(`limit must be an integer`);
|
|
126
|
+
}
|
|
127
|
+
if (limit < 1) {
|
|
128
|
+
throw new TypeError(`limit must be at least one`);
|
|
129
|
+
}
|
|
130
|
+
this.localOptions.limit = limit;
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
59
133
|
|
|
60
134
|
public async first(): Promise<InstanceType<ModelType> | null> {
|
|
61
135
|
const collection = await this.modelConstructor.getCollection();
|
|
62
|
-
const driverOptions =
|
|
63
|
-
ensureSort(driverOptions);
|
|
136
|
+
const driverOptions = this.getDriverOptions();
|
|
64
137
|
const result = await collection.findOne(this.filter, driverOptions);
|
|
65
|
-
if (result ===
|
|
138
|
+
if (result === null) {
|
|
66
139
|
return null;
|
|
67
140
|
}
|
|
68
141
|
const instance = new this.modelConstructor(
|
|
@@ -87,9 +160,13 @@ class Query<ModelType extends typeof BaseModel>
|
|
|
87
160
|
|
|
88
161
|
public async all(): Promise<Array<InstanceType<ModelType>>> {
|
|
89
162
|
const collection = await this.modelConstructor.getCollection();
|
|
90
|
-
const driverOptions =
|
|
91
|
-
|
|
92
|
-
|
|
163
|
+
const driverOptions = this.getDriverOptions();
|
|
164
|
+
const result = await collection
|
|
165
|
+
.find(
|
|
166
|
+
this.filter as Filter<WithId<ModelAttributes<InstanceType<ModelType>>>>,
|
|
167
|
+
driverOptions,
|
|
168
|
+
)
|
|
169
|
+
.toArray();
|
|
93
170
|
return result.map(
|
|
94
171
|
(value) =>
|
|
95
172
|
new this.modelConstructor(
|
|
@@ -106,7 +183,7 @@ class Query<ModelType extends typeof BaseModel>
|
|
|
106
183
|
|
|
107
184
|
public async count(): Promise<number> {
|
|
108
185
|
const collection = await this.modelConstructor.getCollection();
|
|
109
|
-
const driverOptions =
|
|
186
|
+
const driverOptions = this.getDriverOptions();
|
|
110
187
|
return collection.countDocuments(
|
|
111
188
|
this.filter,
|
|
112
189
|
driverOptions as CountOptions,
|
|
@@ -115,7 +192,7 @@ class Query<ModelType extends typeof BaseModel>
|
|
|
115
192
|
|
|
116
193
|
public async distinct<T = unknown>(key: string): Promise<T[]> {
|
|
117
194
|
const collection = await this.modelConstructor.getCollection();
|
|
118
|
-
const driverOptions =
|
|
195
|
+
const driverOptions = this.getDriverOptions();
|
|
119
196
|
return collection.distinct(
|
|
120
197
|
key,
|
|
121
198
|
this.filter,
|
|
@@ -123,13 +200,26 @@ class Query<ModelType extends typeof BaseModel>
|
|
|
123
200
|
);
|
|
124
201
|
}
|
|
125
202
|
|
|
203
|
+
public async explain(verbosity?: ExplainVerbosityLike): Promise<Document> {
|
|
204
|
+
const collection = await this.modelConstructor.getCollection();
|
|
205
|
+
const driverOptions = this.getDriverOptions();
|
|
206
|
+
return collection
|
|
207
|
+
.find(
|
|
208
|
+
this.filter as Filter<WithId<ModelAttributes<InstanceType<ModelType>>>>,
|
|
209
|
+
driverOptions,
|
|
210
|
+
)
|
|
211
|
+
.explain(verbosity);
|
|
212
|
+
}
|
|
213
|
+
|
|
126
214
|
public async *[Symbol.asyncIterator](): AsyncIterableIterator<
|
|
127
215
|
InstanceType<ModelType>
|
|
128
216
|
> {
|
|
129
217
|
const collection = await this.modelConstructor.getCollection();
|
|
130
|
-
const driverOptions =
|
|
131
|
-
|
|
132
|
-
|
|
218
|
+
const driverOptions = this.getDriverOptions();
|
|
219
|
+
for await (const value of collection.find(
|
|
220
|
+
this.filter as Filter<WithId<ModelAttributes<InstanceType<ModelType>>>>,
|
|
221
|
+
driverOptions,
|
|
222
|
+
)) {
|
|
133
223
|
if (value === null) continue;
|
|
134
224
|
yield new this.modelConstructor(
|
|
135
225
|
value,
|
|
@@ -153,6 +243,13 @@ interface InternalModelConstructorOptions {
|
|
|
153
243
|
session?: ClientSession;
|
|
154
244
|
}
|
|
155
245
|
|
|
246
|
+
function ensureSort(options?: FindOptions): void {
|
|
247
|
+
if (!options || options.sort) return;
|
|
248
|
+
options.sort = {
|
|
249
|
+
_id: -1,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
156
253
|
function hasOwn(object: unknown, key: string): boolean {
|
|
157
254
|
return Object.prototype.hasOwnProperty.call(object, key);
|
|
158
255
|
}
|
|
@@ -173,15 +270,17 @@ export class BaseModel {
|
|
|
173
270
|
public readonly createdAt: Date;
|
|
174
271
|
public readonly updatedAt: Date;
|
|
175
272
|
|
|
176
|
-
public $
|
|
273
|
+
public $original: Record<string, unknown>;
|
|
274
|
+
public $attributes: Record<string, unknown>;
|
|
275
|
+
|
|
276
|
+
public $isPersisted = false;
|
|
277
|
+
public $isLocal = true;
|
|
278
|
+
public $isDeleted = false;
|
|
177
279
|
|
|
178
280
|
protected $collection: Collection<
|
|
179
281
|
ModelAttributes<MongodbDocument<unknown>>
|
|
180
282
|
> | null = null;
|
|
181
|
-
protected $originalData: Record<string, unknown>;
|
|
182
|
-
protected $currentData: Record<string, unknown>;
|
|
183
283
|
protected $options: InternalModelConstructorOptions;
|
|
184
|
-
protected $alreadySaved: boolean;
|
|
185
284
|
|
|
186
285
|
public constructor(
|
|
187
286
|
dbObj?: Record<string, unknown>,
|
|
@@ -189,11 +288,11 @@ export class BaseModel {
|
|
|
189
288
|
alreadyExists = false,
|
|
190
289
|
) {
|
|
191
290
|
if (dbObj) {
|
|
192
|
-
this.$
|
|
193
|
-
this.$
|
|
291
|
+
this.$original = alreadyExists === true ? cloneDeep(dbObj) : {};
|
|
292
|
+
this.$attributes = dbObj;
|
|
194
293
|
} else {
|
|
195
|
-
this.$
|
|
196
|
-
this.$
|
|
294
|
+
this.$original = {};
|
|
295
|
+
this.$attributes = {};
|
|
197
296
|
}
|
|
198
297
|
|
|
199
298
|
if (options !== undefined) {
|
|
@@ -201,8 +300,11 @@ export class BaseModel {
|
|
|
201
300
|
this.$collection = options.collection;
|
|
202
301
|
}
|
|
203
302
|
|
|
204
|
-
|
|
205
|
-
|
|
303
|
+
if (alreadyExists) {
|
|
304
|
+
this.$isPersisted = true;
|
|
305
|
+
this.$isLocal = false;
|
|
306
|
+
}
|
|
307
|
+
|
|
206
308
|
// eslint-disable-next-line no-constructor-return
|
|
207
309
|
return new Proxy(this, proxyHandler);
|
|
208
310
|
}
|
|
@@ -319,7 +421,7 @@ export class BaseModel {
|
|
|
319
421
|
ModelAttributes<InstanceType<ModelType>>
|
|
320
422
|
>;
|
|
321
423
|
const result = await collection.findOne(filter, driverOptions);
|
|
322
|
-
if (result ===
|
|
424
|
+
if (result === null) return null;
|
|
323
425
|
const instance = new this(
|
|
324
426
|
result,
|
|
325
427
|
// @ts-expect-error Unavoidable error.
|
|
@@ -357,7 +459,7 @@ export class BaseModel {
|
|
|
357
459
|
ModelAttributes<InstanceType<ModelType>>
|
|
358
460
|
>;
|
|
359
461
|
const result = await collection.findOne(filter, driverOptions);
|
|
360
|
-
if (result ===
|
|
462
|
+
if (result === null) return null;
|
|
361
463
|
const instance = new this(
|
|
362
464
|
result,
|
|
363
465
|
// @ts-expect-error Unavoidable error.
|
|
@@ -431,7 +533,10 @@ export class BaseModel {
|
|
|
431
533
|
this: ModelType,
|
|
432
534
|
filter: Filter<ModelAttributes<InstanceType<ModelType>>> = {},
|
|
433
535
|
options?: ModelAdapterOptions<
|
|
434
|
-
|
|
536
|
+
Omit<
|
|
537
|
+
FindOptions<ModelAttributes<InstanceType<ModelType>>>,
|
|
538
|
+
ForbiddenQueryOptions
|
|
539
|
+
>
|
|
435
540
|
>,
|
|
436
541
|
): Query<ModelType> {
|
|
437
542
|
return new Query(filter, options, this);
|
|
@@ -441,27 +546,34 @@ export class BaseModel {
|
|
|
441
546
|
this: ModelType,
|
|
442
547
|
connection = this.connection,
|
|
443
548
|
): Promise<Collection<ModelAttributes<InstanceType<ModelType>>>> {
|
|
444
|
-
|
|
445
|
-
throw new Error('Model should only be accessed from IoC container');
|
|
446
|
-
}
|
|
549
|
+
assert(this.$database, 'Model should only be accessed from IoC container');
|
|
447
550
|
const connectionInstance = this.$database.connection(connection);
|
|
448
551
|
return connectionInstance.collection(this.collectionName);
|
|
449
552
|
}
|
|
450
553
|
|
|
451
554
|
public [Symbol.for('nodejs.util.inspect.custom')](): unknown {
|
|
452
555
|
return {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
556
|
+
Model: this.constructor.name,
|
|
557
|
+
$original: this.$original,
|
|
558
|
+
$attributes: this.$attributes,
|
|
559
|
+
$isPersisted: this.$isPersisted,
|
|
560
|
+
$isNew: this.$isNew,
|
|
561
|
+
$isLocal: this.$isLocal,
|
|
562
|
+
$isDeleted: this.$isDeleted,
|
|
563
|
+
$dirty: this.$dirty,
|
|
564
|
+
$isDirty: this.$isDirty,
|
|
457
565
|
};
|
|
458
566
|
}
|
|
459
567
|
|
|
568
|
+
public get $isNew(): boolean {
|
|
569
|
+
return !this.$isPersisted;
|
|
570
|
+
}
|
|
571
|
+
|
|
460
572
|
public get $dirty(): Partial<ModelAttributes<this>> {
|
|
461
|
-
return pickBy(this.$
|
|
573
|
+
return pickBy(this.$attributes, (value, key) => {
|
|
462
574
|
return (
|
|
463
|
-
this.$
|
|
464
|
-
!isEqual(this.$
|
|
575
|
+
this.$original[key] === undefined ||
|
|
576
|
+
!isEqual(this.$original[key], value)
|
|
465
577
|
);
|
|
466
578
|
}) as Partial<ModelAttributes<this>>;
|
|
467
579
|
}
|
|
@@ -490,7 +602,7 @@ export class BaseModel {
|
|
|
490
602
|
public $prepareToSet() {
|
|
491
603
|
const dirty = this.$dirty;
|
|
492
604
|
const dirtyEntries = Object.entries(dirty);
|
|
493
|
-
if (dirtyEntries.length === 0) {
|
|
605
|
+
if (dirtyEntries.length === 0 && this.$isPersisted) {
|
|
494
606
|
return null;
|
|
495
607
|
}
|
|
496
608
|
|
|
@@ -499,11 +611,11 @@ export class BaseModel {
|
|
|
499
611
|
// which shouldn't reset the createdAt field.
|
|
500
612
|
const toSet = {} as DataToSet;
|
|
501
613
|
const now = new Date();
|
|
502
|
-
if (this.$
|
|
503
|
-
this.$
|
|
614
|
+
if (this.$attributes.createdAt === undefined) {
|
|
615
|
+
this.$attributes.createdAt = now;
|
|
504
616
|
toSet.createdAt = now;
|
|
505
617
|
}
|
|
506
|
-
this.$
|
|
618
|
+
this.$attributes.updatedAt = now;
|
|
507
619
|
toSet.updatedAt = now;
|
|
508
620
|
|
|
509
621
|
for (const [dirtyKey, dirtyValue] of dirtyEntries) {
|
|
@@ -513,7 +625,7 @@ export class BaseModel {
|
|
|
513
625
|
}
|
|
514
626
|
|
|
515
627
|
public get id() {
|
|
516
|
-
return this.$
|
|
628
|
+
return this.$attributes._id;
|
|
517
629
|
}
|
|
518
630
|
|
|
519
631
|
public get $isDirty(): boolean {
|
|
@@ -521,7 +633,7 @@ export class BaseModel {
|
|
|
521
633
|
}
|
|
522
634
|
|
|
523
635
|
public toJSON(): unknown {
|
|
524
|
-
return this.$
|
|
636
|
+
return this.$attributes;
|
|
525
637
|
}
|
|
526
638
|
|
|
527
639
|
public async save(
|
|
@@ -536,18 +648,18 @@ export class BaseModel {
|
|
|
536
648
|
...options?.driverOptions,
|
|
537
649
|
session: this.$options?.session,
|
|
538
650
|
};
|
|
539
|
-
if (this.$
|
|
651
|
+
if (!this.$isPersisted) {
|
|
540
652
|
const result = await collection.insertOne(toSet, driverOptions);
|
|
541
|
-
this.$
|
|
653
|
+
this.$attributes._id = result.insertedId;
|
|
654
|
+
this.$isPersisted = true;
|
|
542
655
|
} else {
|
|
543
656
|
await collection.updateOne(
|
|
544
|
-
{ _id: this.$
|
|
657
|
+
{ _id: this.$attributes._id },
|
|
545
658
|
{ $set: toSet },
|
|
546
659
|
driverOptions,
|
|
547
660
|
);
|
|
548
661
|
}
|
|
549
|
-
this.$
|
|
550
|
-
this.$alreadySaved = true;
|
|
662
|
+
this.$original = cloneDeep(this.$attributes);
|
|
551
663
|
return true;
|
|
552
664
|
}
|
|
553
665
|
|
|
@@ -562,7 +674,7 @@ export class BaseModel {
|
|
|
562
674
|
};
|
|
563
675
|
const result = await collection.deleteOne(
|
|
564
676
|
{
|
|
565
|
-
_id: this.$
|
|
677
|
+
_id: this.$attributes._id,
|
|
566
678
|
},
|
|
567
679
|
driverOptions,
|
|
568
680
|
);
|
|
@@ -574,7 +686,7 @@ export class BaseModel {
|
|
|
574
686
|
values: NoExtraProperties<Partial<Omit<ModelAttributes<this>, '_id'>>, T>,
|
|
575
687
|
): this {
|
|
576
688
|
Object.entries(values).forEach(([key, value]) => {
|
|
577
|
-
this.$
|
|
689
|
+
this.$attributes[key] = value;
|
|
578
690
|
});
|
|
579
691
|
return this;
|
|
580
692
|
}
|
|
@@ -582,11 +694,11 @@ export class BaseModel {
|
|
|
582
694
|
public fill<T extends Partial<Omit<ModelAttributes<this>, '_id'>>>(
|
|
583
695
|
values: NoExtraProperties<Partial<Omit<ModelAttributes<this>, '_id'>>, T>,
|
|
584
696
|
) {
|
|
585
|
-
const createdAt = this.$
|
|
586
|
-
this.$
|
|
697
|
+
const createdAt = this.$attributes.createdAt;
|
|
698
|
+
this.$attributes = {
|
|
587
699
|
_id: this.id,
|
|
588
700
|
};
|
|
589
|
-
if (createdAt) this.$
|
|
701
|
+
if (createdAt) this.$attributes.createdAt = createdAt;
|
|
590
702
|
return this.merge(values);
|
|
591
703
|
}
|
|
592
704
|
}
|
|
@@ -602,7 +714,6 @@ export class BaseAutoIncrementModel extends BaseModel {
|
|
|
602
714
|
|
|
603
715
|
const toSet = this.$prepareToSet();
|
|
604
716
|
if (toSet === null) return false;
|
|
605
|
-
|
|
606
717
|
const driverOptions = {
|
|
607
718
|
...options?.driverOptions,
|
|
608
719
|
session: this.$options?.session,
|
|
@@ -615,22 +726,23 @@ export class BaseAutoIncrementModel extends BaseModel {
|
|
|
615
726
|
);
|
|
616
727
|
|
|
617
728
|
const doc = await counterCollection.findOneAndUpdate(
|
|
618
|
-
{ _id:
|
|
729
|
+
{ _id: (this.constructor as typeof BaseModel).collectionName },
|
|
619
730
|
{ $inc: { count: 1 } },
|
|
620
|
-
{ ...driverOptions, upsert: true },
|
|
731
|
+
{ ...driverOptions, upsert: true, returnDocument: 'after' },
|
|
621
732
|
);
|
|
622
|
-
|
|
623
|
-
toSet._id =
|
|
733
|
+
assert(doc.value, 'upsert should always create a document');
|
|
734
|
+
toSet._id = doc.value.count;
|
|
624
735
|
await collection.insertOne(toSet, driverOptions);
|
|
625
|
-
this.$
|
|
736
|
+
this.$attributes._id = doc.value.count;
|
|
737
|
+
this.$isPersisted = true;
|
|
626
738
|
} else {
|
|
627
739
|
await collection.updateOne(
|
|
628
|
-
{ _id: this.$
|
|
740
|
+
{ _id: this.$attributes._id },
|
|
629
741
|
{ $set: toSet },
|
|
630
742
|
driverOptions,
|
|
631
743
|
);
|
|
632
744
|
}
|
|
633
|
-
this.$
|
|
745
|
+
this.$original = cloneDeep(this.$attributes);
|
|
634
746
|
return true;
|
|
635
747
|
}
|
|
636
748
|
}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
|
|
3
|
-
export const proxyHandler = {
|
|
4
|
-
get(target: any, prop: string) {
|
|
3
|
+
export const proxyHandler: ProxyHandler<any> = {
|
|
4
|
+
get(target: any, prop: string | symbol) {
|
|
5
5
|
if (typeof target[prop] !== 'undefined') {
|
|
6
6
|
return Reflect.get(target, prop);
|
|
7
7
|
}
|
|
8
|
-
return Reflect.get(target.$
|
|
8
|
+
return Reflect.get(target.$attributes, prop);
|
|
9
9
|
},
|
|
10
|
-
set(target: any, prop: string, value: any) {
|
|
10
|
+
set(target: any, prop: string | symbol, value: any) {
|
|
11
11
|
if (typeof target[prop] !== 'undefined') {
|
|
12
12
|
return Reflect.set(target, prop, value);
|
|
13
13
|
}
|
|
14
|
-
return Reflect.set(target.$
|
|
14
|
+
return Reflect.set(target.$attributes, prop, value);
|
|
15
|
+
},
|
|
16
|
+
ownKeys() {
|
|
17
|
+
throw new Error(
|
|
18
|
+
'Getting model keys is disallowed. If you want to use object spread on the current data, do { ...model.$attributes }',
|
|
19
|
+
);
|
|
15
20
|
},
|
|
16
21
|
};
|