Skip to content

Commit

Permalink
feat: Support asynchronous dynamic defaults (#678)
Browse files Browse the repository at this point in the history
  • Loading branch information
CarlosLozanoHealthCaters authored Feb 7, 2024
1 parent 17c3678 commit 125f207
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 56 deletions.
28 changes: 24 additions & 4 deletions src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,25 +327,45 @@ describe('utils', () => {
});

describe('getDefaultValues', () => {
test('static values', () => {
test('static values', async () => {
const defaults: DefaultsOption<TestDocument> = {
bar: 1,
foo: 'test',
};

const result = getDefaultValues(defaults);
const result = await getDefaultValues(defaults);
expectType<Partial<TestDocument>>(result);
expect(result).toStrictEqual(defaults);
});

test('dynamic values', () => {
test('dynamic values', async () => {
const defaults: DefaultsOption<TestDocument> = () => ({
bar: 1,
foo: 'test',
ham: new Date(),
});

const result = getDefaultValues(defaults);
const result = await getDefaultValues(defaults);
expectType<Partial<TestDocument>>(result);
expect(result.ham instanceof Date).toBeTruthy();
});

test('dynamic values with async function', async () => {
function getDateAsync(): Promise<Date> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(new Date());
}, 10);
});
}

const defaults: DefaultsOption<TestDocument> = async () => ({
bar: 1,
foo: 'test',
ham: await getDateAsync(),
});

const result = await getDefaultValues(defaults);
expectType<Partial<TestDocument>>(result);
expect(result.ham instanceof Date).toBeTruthy();
});
Expand Down
100 changes: 53 additions & 47 deletions src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,43 +382,47 @@ export function build<TSchema extends BaseSchema, TOptions extends SchemaOptions
if (operations.length === 0) {
return;
}
const finalOperations = operations.map((op) => {
let operation = op;
if ('insertOne' in op) {
operation = {
insertOne: {
document: {
...getDefaultValues(model.defaults),
...op.insertOne.document,

const finalOperations = await Promise.all(
operations.map(async (op) => {
let operation = op;
if ('insertOne' in op) {
operation = {
insertOne: {
document: {
...(await getDefaultValues(model.defaults)),
...op.insertOne.document,
},
},
},
};
} else if (
'updateOne' in op &&
op.updateOne.upsert &&
!Array.isArray(op.updateOne.update)
) {
const { update } = op.updateOne;
operation = {
updateOne: {
...op.updateOne,
update: {
...update,
$setOnInsert: cleanSetOnInsert(
{
...getDefaultValues(model.defaults),
...update.$setOnInsert,
},
update
) as NonNullable<UpdateFilter<TSchema>['$setOnInsert']>,
};
} else if (
'updateOne' in op &&
op.updateOne.upsert &&
!Array.isArray(op.updateOne.update)
) {
const { update } = op.updateOne;
operation = {
updateOne: {
...op.updateOne,
update: {
...update,
$setOnInsert: cleanSetOnInsert(
{
...(await getDefaultValues(model.defaults)),
...update.$setOnInsert,
},
update
) as NonNullable<UpdateFilter<TSchema>['$setOnInsert']>,
},
},
},
};
}
return model.timestamps
? timestampBulkWriteOperation(operation, model.timestamps)
: operation;
});
};
}

return model.timestamps
? timestampBulkWriteOperation(operation, model.timestamps)
: operation;
})
);

const result = await model.collection.bulkWrite(
finalOperations as AnyBulkWriteOperation<TSchema>[],
Expand Down Expand Up @@ -827,7 +831,7 @@ export function build<TSchema extends BaseSchema, TOptions extends SchemaOptions
};

const $setOnInsert = cleanSetOnInsert({
...getDefaultValues(model.defaults),
...await getDefaultValues(model.defaults),
...finalUpdate.$setOnInsert,
...created,
}, finalUpdate);
Expand Down Expand Up @@ -869,16 +873,18 @@ export function build<TSchema extends BaseSchema, TOptions extends SchemaOptions
docs: DocumentForInsert<TSchema, TOptions>[],
options?: BulkWriteOptions
): Promise<TSchema[]> {
const documents = docs.map((doc) => {
return {
...(model.timestamps && {
[getTimestampProperty('createdAt', model.timestamps)]: new Date(),
[getTimestampProperty('updatedAt', model.timestamps)]: new Date(),
}),
...getDefaultValues(model.defaults),
...doc,
};
});
const documents = await Promise.all(
docs.map(async (doc) => {
return {
...(model.timestamps && {
[getTimestampProperty('createdAt', model.timestamps)]: new Date(),
[getTimestampProperty('updatedAt', model.timestamps)]: new Date(),
}),
...(await getDefaultValues(model.defaults)),
...doc,
};
})
);

const result = await model.collection.insertMany(
documents as unknown as OptionalUnlessRequiredId<TSchema>[],
Expand Down Expand Up @@ -925,7 +931,7 @@ export function build<TSchema extends BaseSchema, TOptions extends SchemaOptions
[getTimestampProperty('createdAt', model.timestamps)]: new Date(),
[getTimestampProperty('updatedAt', model.timestamps)]: new Date(),
}),
...getDefaultValues(model.defaults),
...(await getDefaultValues(model.defaults)),
...doc,
};

Expand Down
5 changes: 4 additions & 1 deletion src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ export type SchemaTimestampOptions =
}>
| boolean;

export type DefaultsOption<TProperties> = Partial<TProperties> | (() => Partial<TProperties>);
export type DefaultsOption<TProperties> =
| Partial<TProperties>
| (() => Partial<TProperties>)
| (() => Promise<Partial<TProperties>>);

export interface SchemaOptions<TProperties> {
defaults?: DefaultsOption<TProperties>;
Expand Down
10 changes: 6 additions & 4 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ export type DocumentForInsertWithoutDefaults<TSchema, TDefaults extends Partial<
export type SchemaDefaultValues<
TSchema,
TOptions extends SchemaOptions<TSchema>,
> = TOptions['defaults'] extends () => infer ReturnDefaults ? ReturnDefaults : TOptions['defaults'];
> = TOptions['defaults'] extends () => infer ReturnDefaults
? Awaited<ReturnDefaults>
: TOptions['defaults'];

export type DocumentForInsert<
TSchema,
Expand Down Expand Up @@ -282,11 +284,11 @@ export function getIds(ids: Set<string> | readonly (ObjectId | string)[]): Objec

// Checks the type of the model defaults property and if a function, returns
// the result of the function call, otherwise returns the object
export function getDefaultValues<TSchema extends BaseSchema>(
export async function getDefaultValues<TSchema extends BaseSchema>(
defaults?: DefaultsOption<TSchema>
): Partial<TSchema> {
): Promise<Partial<TSchema>> {
if (typeof defaults === 'function') {
return defaults();
return await defaults();
}
if (typeof defaults === 'object') {
return defaults;
Expand Down

0 comments on commit 125f207

Please sign in to comment.