Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/helpers/sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,18 @@ export function sql(strings: TemplateStringsArray, ...keys: any[]) {
if (i < (strings.length - 1)) sqlOutput += '$' + (i + 1);
}
return { sql: sqlOutput, params: keys };
}

/**
* Merges multiple SqlStatement objects into a single one
*/
export function mergeSql(...sqlStatements: SqlStatement[]): SqlStatement {
let sqlOutput = '';
const params = [];
for (const sqls of sqlStatements) {
sqlOutput += sqls.sql + ' ';
params.push(...sqls.params);
}
sqlOutput = sqlOutput.split(/\$\d+/).map((part, i) => i > 0 ? ('$' + i + part) : part).join('');
return { sql: sqlOutput.trim(), params };
}
406 changes: 154 additions & 252 deletions src/main.ts

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/relations/ormRelationGetter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Magic getter constant
// This is used on custom property getters to mark them as ORM relation getters
export const ormRelationGetter = Symbol('ormRelationGetter');

export function isOrmRelationGetter(object: any, property: string) {
const getter = Object.getOwnPropertyDescriptor(object, property)?.get;
if (getter == null) return false;
return ormRelationGetter in getter && getter[ormRelationGetter] != null && getter[ormRelationGetter] == true;
}
10 changes: 5 additions & 5 deletions src/validateSchema.ts → src/tools/validateSchema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { sql, SqlStatement } from './helpers/sql.js';
import { SchemaValidationError } from './errors/SchemaValidationError.js';
import { camelCaseToSnakeCase, suffixId } from './util/strings.js';
import { SchemaValidationResult } from './main.js';
import { BaseEntity } from './entities/BaseEntity.js';
import { sql, SqlStatement } from '../helpers/sql.js';
import { SchemaValidationError } from '../errors/SchemaValidationError.js';
import { camelCaseToSnakeCase, suffixId } from '../util/strings.js';
import { SchemaValidationResult } from '../main.js';
import { BaseEntity } from '../entities/BaseEntity.js';

/**
* Running this function will check the entity definitions against the database schema
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
3 changes: 3 additions & 0 deletions src/types/BaseEntityDefinitions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { BaseEntity } from "../entities/BaseEntity.js";

export type BaseEntityDefinitions = Record<string, BaseEntity>;
6 changes: 6 additions & 0 deletions src/types/EntityTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { BaseEntityDefinitions } from "./BaseEntityDefinitions.js";

// Entity helper types
export type EntityName<ED extends BaseEntityDefinitions> = keyof ED;
export type EntityDefinition<ED extends BaseEntityDefinitions> = ED[EntityName<ED>];
export type EntityByName<ED extends BaseEntityDefinitions, K extends EntityName<ED>> = ED[K];
7 changes: 7 additions & 0 deletions src/types/EvalGeneric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// This type is an identity type: the output is identical to the input, except it forces the TypeScript compiler to evaluate the type
// Collapsing the type is useful both for performance reasons (it makes TypeScript horribly slow otherwise in some scenarios)
// and it has the added benefit of making the types much more readable in error messages and IDE inlay hints
// type EvalGeneric<X> = X extends infer C ? { [K in keyof C]: C[K] } : never;
export type EvalGeneric<X> = unknown extends X
? X
: (X extends infer C ? { [K in keyof C]: C[K] } : never);
10 changes: 10 additions & 0 deletions src/types/FieldTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { BaseEntityDefinitions } from "./BaseEntityDefinitions.js";
import { EntityDefinition } from "./EntityTypes.js";
import { IsNullable } from "./IsNullable.js";

// Helper types to get field properties for a specific entity
export type Fields<ED extends BaseEntityDefinitions, E extends EntityDefinition<ED>> = E['fields'];
export type FieldNames<ED extends BaseEntityDefinitions, E extends EntityDefinition<ED>> = keyof Fields<ED, E>;
export type Field<ED extends BaseEntityDefinitions, E extends EntityDefinition<ED>, N extends FieldNames<ED, E>> = Fields<ED, E>[N];
export type FieldType<ED extends BaseEntityDefinitions, E extends EntityDefinition<ED>, N extends FieldNames<ED, E>> = ReturnType<Field<ED, E, N>['toType']>;
export type FieldNullNever<ED extends BaseEntityDefinitions, E extends EntityDefinition<ED>, N extends FieldNames<ED, E>> = IsNullable<Field<ED, E, N>['nullableOnOutput']>;
28 changes: 28 additions & 0 deletions src/types/InputType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { BaseEntityDefinitions } from "./BaseEntityDefinitions.js";
import { EntityByName, EntityDefinition } from "./EntityTypes.js";
import { EvalGeneric } from "./EvalGeneric.js";
import { FieldType } from "./FieldTypes.js";
import { OutputTypeRef } from "./OutputType.js";
import { WithOptionalId } from "./WithOptionalId.js";

// Define input type for save functions
type InputTypeFields<ED extends BaseEntityDefinitions, E extends EntityDefinition<ED>> =
{ -readonly [N in keyof E['fields'] as E['fields'][N]['nullableOnInput'] extends true ? never : N]: FieldType<ED, E, N> } // mandatory properties
& { -readonly [N in keyof E['fields'] as E['fields'][N]['nullableOnInput'] extends true ? N : never]?: FieldType<ED, E, N> | null | undefined }; // optionals

type InputTypeOneToOneOwned<ED extends BaseEntityDefinitions, E extends EntityDefinition<ED>> =
{ -readonly [N in keyof E['oneToOneOwned'] as E['oneToOneOwned'][N]['nullable'] extends true ? never : N]: OutputTypeRef<ED, EntityByName<ED, E['oneToOneOwned'][N]['entity']>> } // mandatory properties
& { -readonly [N in keyof E['oneToOneOwned'] as E['oneToOneOwned'][N]['nullable'] extends true ? N : never]?: OutputTypeRef<ED, EntityByName<ED, E['oneToOneOwned'][N]['entity']>> | null | undefined }; // optionals

type InputTypeManyToOne<ED extends BaseEntityDefinitions, E extends EntityDefinition<ED>> =
{ -readonly [N in keyof E['manyToOne'] as E['manyToOne'][N]['nullable'] extends true ? never : N]: OutputTypeRef<ED, EntityByName<ED, E['manyToOne'][N]['entity']>> } // mandatory properties
& { -readonly [N in keyof E['manyToOne'] as E['manyToOne'][N]['nullable'] extends true ? N : never]?: OutputTypeRef<ED, EntityByName<ED, E['manyToOne'][N]['entity']>> | null | undefined }; // optionals

export type InputTypeWithId<ED extends BaseEntityDefinitions, E extends EntityDefinition<ED>> =
InputTypeFields<ED, E> & InputTypeOneToOneOwned<ED, E> & InputTypeManyToOne<ED, E> & {
-readonly [N in keyof E['oneToOneInverse']]?: any /* Allow feeding something, but we don't care what exactly since we won't save it. We could narrow this to identical to OutputType<> */
} & {
-readonly [N in keyof E['oneToMany']]?: any /* Allow feeding something, but we don't care what exactly since we won't save it. We could narrow this to identical to OutputType<> */
};

export type InputType<ED extends BaseEntityDefinitions, E extends EntityDefinition<ED>> = EvalGeneric<WithOptionalId<InputTypeWithId<ED, E>>>;
1 change: 1 addition & 0 deletions src/types/IsNullable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type IsNullable<T extends boolean> = T extends true ? null : never;
22 changes: 22 additions & 0 deletions src/types/OutputType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { BaseEntityDefinitions } from "./BaseEntityDefinitions.js";
import { EntityByName, EntityDefinition } from "./EntityTypes.js";
import { EvalGeneric } from "./EvalGeneric.js";
import { FieldNullNever, FieldType } from "./FieldTypes.js";
import { IsNullable } from "./IsNullable.js";

// Prep the output type for a specific entity
// Defines a type for every field on a given entity, including correctly nullability setting
// Defines the types for all relations, which will something like Promise<TargetEntity> or Promise<TargetEntity[]>, depending on the relation type, with correct nullability
export type OutputType<ED extends BaseEntityDefinitions, E extends EntityDefinition<ED>> = EvalGeneric<{
-readonly [N in keyof E['fields']]: FieldType<ED, E, N> | FieldNullNever<ED, E, N>
} & {
-readonly [N in keyof E['oneToOneOwned']]: Promise<OutputType<ED, EntityByName<ED, E['oneToOneOwned'][N]['entity']>>> | IsNullable<E['oneToOneOwned'][N]['nullable']>
} & {
-readonly [N in keyof E['oneToOneInverse']]: Promise<OutputType<ED, EntityByName<ED, E['oneToOneInverse'][N]['entity']>> | IsNullable<E['oneToOneInverse'][N]['nullable']>>
} & {
-readonly [N in keyof E['manyToOne']]: Promise<OutputType<ED, EntityByName<ED, E['manyToOne'][N]['entity']>>> | IsNullable<E['manyToOne'][N]['nullable']>
} & {
[N in keyof E['oneToMany']]: Promise<OutputType<ED, EntityByName<ED, E['oneToMany'][N]['entity']>>[]>
}>;

export type OutputTypeRef<ED extends BaseEntityDefinitions, E extends EntityDefinition<ED>> = Promise<OutputType<ED, E>> | OutputType<ED, E>;
17 changes: 17 additions & 0 deletions src/types/RawSqlType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { CamelToSnakeCase, IdSuffixed } from "../util/strings.js";
import { BaseEntityDefinitions } from "./BaseEntityDefinitions.js";
import { EntityDefinition } from "./EntityTypes.js";
import { IsNullable } from "./IsNullable.js";

// Construct the RawSqlType, which is a type definition that encompasses what is returned from a SQL-query
// Currently any types for OneToMany relations that aren't defined as a ManyToOne on the other side are missing
// (e.g if B defines a OneToMany to A, then A will not have a field for this relation, even though in reality there will be a b_id column in the table for A)
// The clunky N extends string stuff is to ensure TypeScript does not get confused. Even though the keys of FieldNames<> and such are always strings,
// for some reason TS tries to use the default key type (symbol | number | string) instead of the more narrow string we know it to be.
export type RawSqlType<ED extends BaseEntityDefinitions, E extends EntityDefinition<ED>> = {
-readonly [N in keyof E['fields'] as CamelToSnakeCase<Extract<N, string>>]: ReturnType<E['fields'][N]['toType']> | IsNullable<E['fields'][N]['nullableOnOutput']>
} & {
-readonly [N in keyof E['oneToOneOwned'] as IdSuffixed<CamelToSnakeCase<Extract<N, string>>>]: number | IsNullable<E['oneToOneOwned'][N]['nullable']>
} & {
-readonly [N in keyof E['manyToOne'] as IdSuffixed<CamelToSnakeCase<Extract<N, string>>>]: number | IsNullable<E['manyToOne'][N]['nullable']>
};
1 change: 1 addition & 0 deletions src/types/WithOptionalId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type WithOptionalId<X extends {}> = Omit<X, 'id'> & { id?: string | null };
3 changes: 3 additions & 0 deletions src/util/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function strictKeysOfObject<T extends object>(input: T): Array<keyof T> {
return Object.keys(input) as any;
}
4 changes: 2 additions & 2 deletions tests/testHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { _isOrmRelationGetter } from '../src/main';
import { isOrmRelationGetter } from '../src/relations/ormRelationGetter';

export function hideRelations<T>(input: T): T {
const output = {} as T;
for (const key in input) {
if (!_isOrmRelationGetter(input, key)) {
if (!isOrmRelationGetter(input, key)) {
output[key] = input[key];
}
}
Expand Down
Loading