Skip to content

Commit

Permalink
feat(orm): support passing type to Database.persistAs/Database.remove…
Browse files Browse the repository at this point in the history
…As, DatabaseSession.addAs

This allows to specify a type manually in case of having several interfaces registered with overlapping shape.

closes #571
  • Loading branch information
marcj committed Jun 5, 2024
1 parent 1e8d61d commit 6679aba
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 54 deletions.
24 changes: 21 additions & 3 deletions packages/orm/src/database-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,26 @@
*/

import { OrmEntity } from './type.js';
import { AbstractClassType, arrayRemoveItem, ClassType, getClassName, getClassTypeFromInstance, isClass, stringifyValueWithType } from '@deepkit/core';
import { is, isSameType, ItemChanges, PrimaryKeyFields, ReceiveType, ReflectionClass, ReflectionKind, stringifyType, Type } from '@deepkit/type';
import {
AbstractClassType,
arrayRemoveItem,
ClassType,
getClassName,
getClassTypeFromInstance,
isClass,
stringifyValueWithType,
} from '@deepkit/core';
import {
is,
isSameType,
ItemChanges,
PrimaryKeyFields,
ReceiveType,
ReflectionClass,
ReflectionKind,
stringifyType,
Type,
} from '@deepkit/type';
import { Query } from './query.js';
import { DatabaseSession, DatabaseTransaction } from './database-session.js';

Expand Down Expand Up @@ -161,7 +179,7 @@ export class DatabaseEntityRegistry {
}

} else {
//its a regular class
//it's a regular class
return ReflectionClass.from(getClassTypeFromInstance(item));
}

Expand Down
139 changes: 96 additions & 43 deletions packages/orm/src/database-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,14 @@ export class DatabaseSessionRound<ADAPTER extends DatabaseAdapter> {
protected addQueue = new Set<OrmEntity>();
protected removeQueue = new Set<OrmEntity>();

protected addQueueResolved: [ReflectionClass<any>, OrmEntity][] = [];
protected removeQueueResolved: [ReflectionClass<any>, OrmEntity][] = [];

protected inCommit: boolean = false;
protected committed: boolean = false;

constructor(
protected round: number = 0,
protected session: DatabaseSession<any>,
protected eventDispatcher: EventDispatcherInterface,
public logger: DatabaseLogger,
Expand All @@ -67,59 +71,83 @@ export class DatabaseSessionRound<ADAPTER extends DatabaseAdapter> {
return this.committed;
}

public add(...items: OrmEntity[]): void {
public add(items: Iterable<OrmEntity>, classSchema?: ReflectionClass<any>): void {
if (this.isInCommit()) throw new Error('Already in commit. Can not change queues.');

for (const item of items) {
if (this.removeQueue.has(item)) continue;
if (this.addQueue.has(item)) continue;
const old = typeSettings.unpopulatedCheck;
typeSettings.unpopulatedCheck = UnpopulatedCheck.None;
try {
for (const item of items) {
if (this.removeQueue.has(item)) continue;
if (this.addQueue.has(item)) continue;

this.addQueue.add(item);
this.addQueue.add(item);

for (const dep of this.getReferenceDependencies(item)) {
this.add(dep);
const thisClassSchema = classSchema || this.session.entityRegistry.getFromInstance(item);
this.addQueueResolved.push([thisClassSchema, item]);

for (const [schema, dep] of this.getReferenceDependenciesWithSchema(thisClassSchema, item)) {
this.add([dep], schema);
}
}
} finally {
typeSettings.unpopulatedCheck = old;
}
}

protected getReferenceDependenciesWithSchema<T extends OrmEntity>(classSchema: ReflectionClass<any>, item: T): [ReflectionClass<any>, OrmEntity][] {
const result: [ReflectionClass<any>, OrmEntity][] = [];

for (const reference of classSchema.getReferences()) {
if (reference.isBackReference()) continue;
const v = item[reference.getNameAsString() as keyof T] as any;
if (v == undefined) continue;
if (!isReferenceInstance(v)) result.push([reference.getResolvedReflectionClass(), v]);
}

return result;
}

protected getReferenceDependencies<T extends OrmEntity>(item: T): OrmEntity[] {
protected getReferenceDependencies<T extends OrmEntity>(classSchema: ReflectionClass<any>, item: T): OrmEntity[] {
const result: OrmEntity[] = [];
const classSchema = this.session.entityRegistry.getFromInstance(item);

const old = typeSettings.unpopulatedCheck;
typeSettings.unpopulatedCheck = UnpopulatedCheck.None;
try {
for (const reference of classSchema.getReferences()) {
if (reference.isBackReference()) continue;

//todo, check if join was populated. will throw otherwise
const v = item[reference.getNameAsString() as keyof T] as any;
if (v == undefined) continue;

// if (reference.isArray) {
// if (isArray(v)) {
// for (const i of v) {
// if (isReference(v)) continue;
// if (i instanceof reference.getResolvedClassType()) result.push(i);
// }
// }
// } else {
if (!isReferenceInstance(v)) result.push(v);
// }
}
} finally {
typeSettings.unpopulatedCheck = old;
for (const reference of classSchema.getReferences()) {
if (reference.isBackReference()) continue;
const v = item[reference.getNameAsString() as keyof T] as any;
if (v == undefined) continue;

// if (reference.isArray) {
// if (isArray(v)) {
// for (const i of v) {
// if (isReference(v)) continue;
// if (i instanceof reference.getResolvedClassType()) result.push(i);
// }
// }
// } else {
if (!isReferenceInstance(v)) result.push(v);
// }
}

return result;
}

public remove(...items: OrmEntity[]) {
public remove(items: OrmEntity[], schema?: ReflectionClass<any>) {
if (this.isInCommit()) throw new Error('Already in commit. Can not change queues.');

const removeAdded: OrmEntity[] = [];

for (const item of items) {
this.removeQueue.add(item);
this.addQueue.delete(item);
this.removeQueueResolved.push([schema || this.session.entityRegistry.getFromInstance(item), item]);

if (this.addQueue.has(item)) {
this.addQueue.delete(item);
removeAdded.push(item);
}
}

if (removeAdded.length) {
this.addQueueResolved = this.addQueueResolved.filter(v => !removeAdded.includes(v[1]));
}
}

Expand All @@ -138,7 +166,7 @@ export class DatabaseSessionRound<ADAPTER extends DatabaseAdapter> {
}

protected async doDelete(persistence: DatabasePersistence) {
for (const [classSchema, items] of getClassSchemaInstancePairs(this.removeQueue.values())) {
for (const [classSchema, items] of getClassSchemaInstancePairs(this.removeQueueResolved)) {
if (this.eventDispatcher.hasListeners(DatabaseSession.onDeletePre)) {
const event = new UnitOfWorkEvent(classSchema, this.session, items);
await this.eventDispatcher.dispatch(DatabaseSession.onDeletePre, event);
Expand All @@ -163,9 +191,8 @@ export class DatabaseSessionRound<ADAPTER extends DatabaseAdapter> {
typeSettings.unpopulatedCheck = UnpopulatedCheck.None;

try {
for (const item of this.addQueue.values()) {
const classSchema = this.session.entityRegistry.getFromInstance(item);
sorter.add(item, classSchema, this.getReferenceDependencies(item));
for (const [classSchema, item] of this.addQueueResolved) {
sorter.add(item, classSchema, this.getReferenceDependencies(classSchema, item));
}

sorter.sort();
Expand Down Expand Up @@ -291,6 +318,7 @@ export abstract class DatabaseTransaction {

export class DatabaseSession<ADAPTER extends DatabaseAdapter = DatabaseAdapter> {
public readonly id = SESSION_IDS++;
public round: number = 0;
public withIdentityMap = true;

/**
Expand Down Expand Up @@ -468,7 +496,7 @@ export class DatabaseSession<ADAPTER extends DatabaseAdapter = DatabaseAdapter>
}

protected enterNewRound() {
this.rounds.push(new DatabaseSessionRound(this, this.eventDispatcher, this.logger, this.withIdentityMap ? this.identityMap : undefined));
this.rounds.push(new DatabaseSessionRound(this.round++, this, this.eventDispatcher, this.logger, this.withIdentityMap ? this.identityMap : undefined));
}

/**
Expand All @@ -481,20 +509,45 @@ export class DatabaseSession<ADAPTER extends DatabaseAdapter = DatabaseAdapter>
this.enterNewRound();
}

this.getCurrentRound().add(...items);
this.getCurrentRound().add(items);
}

/**
* Adds a item to the remove queue. Use session.commit() to remove queued items from the database all at once.
* Adds a single or multiple items for a particular type to the to add/update queue. Use session.commit() to persist all queued items to the database.
*
* This works like Git: you add files, and later commit all in one batch.
*/
public addAs<T extends OrmEntity>(items: T[], type?: ReceiveType<T> | ReflectionClass<any>) {
if (this.getCurrentRound().isInCommit()) {
this.enterNewRound();
}

this.getCurrentRound().add(items, ReflectionClass.from(type));
}

/**
* Adds item to the remove queue. Use session.commit() to remove queued items from the database all at once.
*/
public remove(...items: OrmEntity[]) {
if (this.getCurrentRound().isInCommit()) {
this.enterNewRound();
}

this.getCurrentRound().remove(...items);
this.getCurrentRound().remove(items);
}

/**
* Adds item to the remove queue for a particular type. Use session.commit() to remove queued items from the database all at once.
*/
public removeAs<T extends OrmEntity>(items: T[], type?: ReceiveType<T> | ReflectionClass<any>) {
if (this.getCurrentRound().isInCommit()) {
this.enterNewRound();
}

this.getCurrentRound().remove(items, ReflectionClass.from(type));
}


/**
* Resets all scheduled changes (add() and remove() calls).
*
Expand Down Expand Up @@ -558,7 +611,7 @@ export class DatabaseSession<ADAPTER extends DatabaseAdapter = DatabaseAdapter>
if (this.withIdentityMap) {
for (const map of this.identityMap.registry.values()) {
for (const item of map.values()) {
round.add(item.ref);
round.add([item.ref]);
}
}
}
Expand Down
33 changes: 30 additions & 3 deletions packages/orm/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@
* You should have received a copy of the MIT License along with this program.
*/

import { AbstractClassType, ClassType, forwardTypeArguments, getClassName, getClassTypeFromInstance } from '@deepkit/core';
import {
AbstractClassType,
ClassType,
forwardTypeArguments,
getClassName,
getClassTypeFromInstance,
} from '@deepkit/core';
import {
entityAnnotation,
EntityOptions,
Expand All @@ -19,7 +25,7 @@ import {
ReflectionClass,
ReflectionKind,
resolveReceiveType,
Type
Type,
} from '@deepkit/type';
import { DatabaseAdapter, DatabaseEntityRegistry, MigrateOptions } from './database-adapter.js';
import { DatabaseSession } from './database-session.js';
Expand Down Expand Up @@ -89,6 +95,7 @@ function setupVirtualForeignKey(database: Database, virtualForeignKeyConstraint:
await virtualForeignKeyConstraint.onQueryDelete(event);
});
}

/**
* Database abstraction. Use createSession() to create a work session with transaction support.
*
Expand Down Expand Up @@ -142,7 +149,7 @@ export class Database<ADAPTER extends DatabaseAdapter = DatabaseAdapter> {

constructor(
public readonly adapter: ADAPTER,
schemas: (Type | ClassType | ReflectionClass<any>)[] = []
schemas: (Type | ClassType | ReflectionClass<any>)[] = [],
) {
this.entityRegistry.add(...schemas);
if (Database.registry) Database.registry.push(this);
Expand Down Expand Up @@ -353,6 +360,16 @@ export class Database<ADAPTER extends DatabaseAdapter = DatabaseAdapter> {
await session.commit();
}

/**
* Same as persist(), but allows to specify the type that should be used for the given items.
*/
public async persistAs<T extends OrmEntity>(items: T[], type?: ReceiveType<T>) {
const session = this.createSession();
session.withIdentityMap = false;
session.addAs(items, ReflectionClass.from(type));
await session.commit();
}

/**
* Simple direct remove. The persistence layer (batch) removes all given items.
* This is different to createSession()+remove() in a way that `DatabaseSession.remove` adds the given items to the queue
Expand All @@ -367,6 +384,16 @@ export class Database<ADAPTER extends DatabaseAdapter = DatabaseAdapter> {
session.remove(...items);
await session.commit();
}

/**
* Same as remove(), but allows to specify the type that should be used for the given items.
*/
public async removeAs<T extends OrmEntity>(items: T[], type?: ReceiveType<T>) {
const session = this.createSession();
session.withIdentityMap = false;
session.removeAs(items, ReflectionClass.from(type));
await session.commit();
}
}

export interface ActiveRecordClassType {
Expand Down
6 changes: 2 additions & 4 deletions packages/orm/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,14 @@ import { OrmEntity } from './type.js';
import sift from 'sift';
import { FilterQuery } from './query.js';
import { getInstanceStateFromItem } from './identity-map.js';
import { getClassTypeFromInstance } from '@deepkit/core';

export type FlattenIfArray<T> = T extends Array<any> ? T[0] : T;
export type FieldName<T> = keyof T & string;

export function getClassSchemaInstancePairs<T extends OrmEntity>(items: Iterable<T>): Map<ReflectionClass<any>, T[]> {
export function getClassSchemaInstancePairs<T extends OrmEntity>(items: Iterable<[ReflectionClass<any>, T]>): Map<ReflectionClass<any>, T[]> {
const map = new Map<ReflectionClass<any>, T[]>();

for (const item of items) {
const classSchema = ReflectionClass.from(getClassTypeFromInstance(item));
for (const [classSchema, item] of items) {
let items = map.get(classSchema);
if (!items) {
items = [];
Expand Down
29 changes: 29 additions & 0 deletions packages/orm/tests/database.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,32 @@ test('memory-db', async () => {
await database.query(s).deleteMany();
expect(await (await database.query(s).find()).length).toBe(0);
});

test('persistAs', async () => {
interface X {
id: number & PrimaryKey;
name: string;
}

interface Y {
id: number & PrimaryKey;
name: string;
}

const database = new Database(new MemoryDatabaseAdapter());
database.register<X>({ name: 'x' });
database.register<Y>({ name: 'y' });

await database.persistAs<X>([{ id: 1, name: 'Peter' }, { id: 2, name: 'Peter2' }]);
await database.persistAs<X>([{ id: 3, name: 'Peter3' }]);
await database.persistAs<Y>([{ id: 1, name: 'John' }]);

expect(await database.query<X>().count()).toBe(3);
expect(await database.query<Y>().count()).toBe(1);

await database.removeAs<X>([{ id: 1, name: 'Peter' }]);
expect(await database.query<X>().count()).toBe(2);

await database.removeAs<Y>([{ id: 1, name: 'John' }]);
expect(await database.query<Y>().count()).toBe(0);
});
Loading

0 comments on commit 6679aba

Please sign in to comment.