diff --git a/.gitignore b/.gitignore index 538a70ef..c591dde2 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ test/types/*.js test/types/*.map src/decorators.js* src/data_types.js* - +src/raw.js* diff --git a/index.js b/index.js index 8df8d43c..94505cc9 100644 --- a/index.js +++ b/index.js @@ -12,7 +12,7 @@ const { heresql } = require('./src/utils/string'); const Hint = require('./src/hint'); const Realm = require('./src/realm'); const Decorators = require('./src/decorators'); -const Raw = require('./src/raw'); +const Raw = require('./src/raw').default; const { MysqlDriver, PostgresDriver, SqliteDriver, AbstractDriver } = require('./src/drivers'); const { isBone } = require('./src/utils'); diff --git a/src/adapters/sequelize.d.ts b/src/adapters/sequelize.d.ts index f592da76..e06bfa6c 100644 --- a/src/adapters/sequelize.d.ts +++ b/src/adapters/sequelize.d.ts @@ -2,7 +2,7 @@ import { Attributes, Literal, OperatorCondition, BoneOptions, ResultSet, Raw, SetOptions, BeforeHooksType, AfterHooksType, - QueryOptions, OrderOptions, QueryResult, Values as CommonValues, + QueryOptions, OrderOptions, QueryResult, Values as CommonValues, BoneColumns, InstanceColumns, } from '../types/common'; import { AbstractBone } from '../types/abstract_bone'; import { Spell } from '../spell'; @@ -21,21 +21,21 @@ interface BaseSequelizeConditions extends QueryO where?: WhereConditions; order?: OrderOptions; limit?: number; - attributes?: string | Raw | Array<[keyof Extract>, Literal>] | string | Raw> | [keyof Extract>, Literal>]; + attributes?: BoneColumns | Array | string | Raw> | string | Raw; offset?: number; } type SequelizeUpdateOptions = BaseSequelizeConditions & { - fields?: Array<[keyof Extract>, Literal>] | string | Raw> | [keyof Extract>, Literal>]; + fields?: BoneColumns | Array | string | Raw> | string; } interface SequelizeInstanceUpdateOptions extends QueryOptions { - attributes?: string | Raw | Array<[keyof Extract, Literal>] | string | Raw> | [keyof Extract, Literal>]; + attributes?: [keyof Extract, Literal>] | string | Raw | Array<[keyof Extract, Literal>] | string | Raw>; fields?: Array<[keyof Extract, Literal>] | string | Raw> | [keyof Extract, Literal>]; } interface SequelizeConditions extends BaseSequelizeConditions { - group?: string | string[] | Raw; + group?: BoneColumns | BoneColumns[] | Raw | string; having?: WhereConditions | string | { [key:string]: Literal | Literal[] } | Raw; include?: string | Raw; } @@ -82,7 +82,7 @@ export class SequelizeBone extends AbstractBone { static getTableName(): boolean; - static removeAttribute(name: string): void; + static removeAttribute(this: T, name?: BoneColumns): void; /** * @@ -122,7 +122,8 @@ export class SequelizeBone extends AbstractBone { */ static setScope(this: T, name: (string | ((...args: any[]) => SequelizeConditions) | SequelizeConditions | Array>), ...args: any[]): void; - static aggregate(this: T, name: string, func: aggregators, options?: SequelizeConditions): Spell; + static aggregate(this: T, name: BoneColumns, func: aggregators, options?: SequelizeConditions): Spell; + static aggregate(this: T, name: Raw | '*', func: aggregators, options?: SequelizeConditions): Spell; static build(this: T, values: Values, options?: BoneOptions): InstanceType; @@ -133,24 +134,30 @@ export class SequelizeBone extends AbstractBone { */ static bulkBuild(this:T, valueSets: Array>, options?: BoneOptions): Array>; - static count(this: T, name?: string): Spell | number>; + static count(this: T, name?: BoneColumns): Spell | number>; + static count(this: T, name?: Raw | '*'): Spell | number>; static count(this: T, conditions?: SequelizeConditions): Spell | number>; static decrement( this: T, - fields: string | Array | { [Property in keyof Extract, Literal>]?: number }, + fields: { [Property in keyof Extract, Literal>]?: number } | string | Array | string> , options?: SequelizeConditions ): Spell; static increment( this: T, - fields: string | Array | { [Property in keyof Extract, Literal>]?: number }, + fields: { [Property in keyof Extract, Literal>]?: number } | string | Array | string> , options?: SequelizeConditions ): Spell; - static max(this: T, filed: string, options?: SequelizeConditions): Promise; - static min(this: T, filed: string, options?: SequelizeConditions): Promise; - static sum(this: T, filed: string, options?: SequelizeConditions): Promise; + static max(this: T, field: BoneColumns, options?: SequelizeConditions): Promise; + static max(this: T, field: Raw, options?: SequelizeConditions): Promise; + + static min(this: T, field: BoneColumns, options?: SequelizeConditions): Promise; + static min(this: T, field: Raw, options?: SequelizeConditions): Promise; + + static sum(this: T, field: BoneColumns, options?: SequelizeConditions): Promise; + static sum(this: T, field: Raw, options?: SequelizeConditions): Promise; static destroy(this: T, options?: DestroyOptions): Promise | number>; static bulkDestroy(this: T, options?: DestroyOptions): Spell; @@ -184,18 +191,32 @@ export class SequelizeBone extends AbstractBone { get dataValues(): { [key: string]: Literal }; where(): { [key: string]: number | bigint | string }; + set>(this: T, key: Key, value: T[Key]): void; set(this: T, key: Key, value: T[Key]): void; + + get>(this: T, key?: Key): T[Key]; get(this: T, key?: Key): T[Key]; + + setDataValue>(this: T, key: Key, value: T[Key]): void; setDataValue(this: T, key: Key, value: T[Key]): void; + getDataValue(this: T): T; + getDataValue>(this: T, key: Key): T[Key]; getDataValue(this: T, key: Key): T[Key]; - previous(key?: string): Literal | Literal[] | { [key: string]: Literal | Literal[] }; + + previous>(this: T, key?: Key): Literal | Literal[] | { [Property in keyof Extract]?: Literal | Literal[] }; + previous(this: T, key?: Key): Literal | Literal[] | { [Property in keyof Extract]?: Literal | Literal[] }; + isSoftDeleted(): boolean; - increment(field: string | string[] | { [Property in keyof Extract]?: number }, options?: QueryOptions): Spell; - decrement(field: string | string[] | { [Property in keyof Extract]?: number }, options?: QueryOptions): Spell; + increment(field: InstanceColumns | Array> | { [Property in keyof Extract]?: number }, options?: QueryOptions): Spell; + increment(field: string | Raw | Array, options?: QueryOptions): Spell; + decrement(field: InstanceColumns | Array> | { [Property in keyof Extract]?: number }, options?: QueryOptions): Spell; + decrement(field: string | Raw | Array , options?: QueryOptions): Spell; destroy(options?: SequelizeDestroyOptions): Promise; - update(this: T, changes?: { [key: string]: Literal } | { [Property in keyof Extract]?: Literal }, opts?: SequelizeInstanceUpdateOptions): Promise; + update(this: T, changes?: { [Property in keyof Extract]?: Literal }, opts?: SequelizeInstanceUpdateOptions): Promise; + update(this: T, changes?: { [key: string]: Literal }, opts?: SequelizeInstanceUpdateOptions): Promise; + } export const sequelize: (Bone: AbstractBone) => typeof SequelizeBone; diff --git a/src/adapters/sequelize.js b/src/adapters/sequelize.js index 07c2ef39..66992034 100644 --- a/src/adapters/sequelize.js +++ b/src/adapters/sequelize.js @@ -2,7 +2,7 @@ const { setupSingleHook } = require('../setup_hooks'); const { compose, isPlainObject } = require('../utils'); -const Raw = require('../raw'); +const Raw = require('../raw').default; function translateOptions(spell, options) { const { attributes, where, group, order, offset, limit, include, having } = options; diff --git a/src/bone.d.ts b/src/bone.d.ts index f9ac291d..1a34ad2e 100644 --- a/src/bone.d.ts +++ b/src/bone.d.ts @@ -1,6 +1,6 @@ import { Spell } from './spell'; import { AbstractBone } from './types/abstract_bone'; -import { Collection, Literal, QueryOptions, ResultSet, WhereConditions } from './types/common'; +import { BoneColumns, Collection, Literal, QueryOptions, Raw, ResultSet, Values, WhereConditions } from './types/common'; export default class Bone extends AbstractBone { @@ -26,7 +26,8 @@ export default class Bone extends AbstractBone { static findOne(this: T, primaryKey: number | number[] | bigint): Spell | null>; static findOne(this: T, ): Spell | null>; - static sum(this: T, name?: string): Spell | number>; + static sum(this: T, name?: BoneColumns): Spell | number>; + static sum(this: T, name?: Raw): Spell | number>; /** * restore rows @@ -36,12 +37,12 @@ export default class Bone extends AbstractBone { * @param conditions query conditions * @param opts query options */ - static restore(this: T, conditions: Object, opts?: QueryOptions): Spell; + static restore(this: T, conditions: WhereConditions, opts?: QueryOptions): Spell; /** * UPDATE rows. */ - static update(this: T, whereConditions: WhereConditions, values?: Object, opts?: QueryOptions): Spell; + static update(this: T, whereConditions: WhereConditions, values?: Values> & Partial, Literal>>, opts?: QueryOptions): Spell; /** * Discard all the applied scopes. diff --git a/src/bone.js b/src/bone.js index f2c41df6..17075cd9 100644 --- a/src/bone.js +++ b/src/bone.js @@ -14,7 +14,7 @@ require('reflect-metadata'); const { default: DataTypes } = require('./data_types'); const Collection = require('./collection'); const Spell = require('./spell'); -const Raw = require('./raw'); +const Raw = require('./raw').default; const { capitalize, camelCase, snakeCase } = require('./utils/string'); const { hookNames, setupSingleHook } = require('./setup_hooks'); const { diff --git a/src/data_types.ts b/src/data_types.ts index fbaaa54a..c50e56ab 100644 --- a/src/data_types.ts +++ b/src/data_types.ts @@ -1,8 +1,6 @@ -'use strict'; - -const util = require('util'); +import Raw from './raw'; +import util from 'util'; const invokableFunc = require('./utils/invokable'); -const Raw = require('./raw'); export enum LENGTH_VARIANTS { tiny = 'tiny', @@ -67,7 +65,7 @@ class STRING extends DataType { return chunks.join(' '); } - uncast(value: string | typeof Raw | null): string { + uncast(value: string | Raw | null): string | Raw { if (value == null || value instanceof Raw) return value; return '' + value; } @@ -306,13 +304,14 @@ class DATE extends DataType { return this._round(value); } - uncast(value: null | typeof Raw | string | Date, _strict?: boolean): string | Date { + uncast(value: null | Raw | string | Date | { toDate: () => Date }, _strict?: boolean): string | Date | Raw { const originValue = value; - if (value == null || value instanceof Raw) return value; - if (typeof value.toDate === 'function') { - value = value.toDate(); - } + // type narrowing doesn't handle `return value` correctly + if (value == null) return value as null | undefined; + if (value instanceof Raw) return value; + // Date | Moment + if (typeof value === 'object' && 'toDate' in value) value = value.toDate(); // @deprecated // vaguely standard date formats such as 2021-10-15 15:50:02,548 @@ -324,8 +323,8 @@ class DATE extends DataType { // 1634611135776 // '2021-10-15T08:38:43.877Z' - if (!(value instanceof Date)) value = new Date(value); - if (isNaN(value)) throw new Error(util.format('invalid date: %s', originValue)); + if (!(value instanceof Date)) value = new Date((value as string)); + if (isNaN((value as any))) throw new Error(util.format('invalid date: %s', originValue)); return this._round(value); } diff --git a/src/drivers/abstract/spellbook.js b/src/drivers/abstract/spellbook.js index 6079daa9..ca1d07b3 100644 --- a/src/drivers/abstract/spellbook.js +++ b/src/drivers/abstract/spellbook.js @@ -4,7 +4,7 @@ const SqlString = require('sqlstring'); const { copyExpr, findExpr, walkExpr } = require('../../expr'); const { formatExpr, formatConditions, collectLiteral, isAggregatorExpr } = require('../../expr_formatter'); -const Raw = require('../../raw'); +const Raw = require('../../raw').default; /** * Create a subquery to make sure OFFSET and LIMIT on left table takes effect. diff --git a/src/drivers/postgres/data_types.js b/src/drivers/postgres/data_types.js index d67a4308..35f8548a 100644 --- a/src/drivers/postgres/data_types.js +++ b/src/drivers/postgres/data_types.js @@ -2,7 +2,7 @@ const { default: DataTypes } = require('../../data_types'); const util = require('util'); -const Raw = require('../../raw'); +const Raw = require('../../raw').default; class Postgres_DATE extends DataTypes.DATE { diff --git a/src/expr.js b/src/expr.js index 3c4a96cc..bc6e74ab 100644 --- a/src/expr.js +++ b/src/expr.js @@ -1,5 +1,7 @@ 'use strict'; +const Raw = require('./raw').default; + /** * This module contains a simple SQL expression parser which parses `select_expr` and `expr` in `WHERE`/`HAVING`/`ON` conditions. Most of {@link Spell}'s functionalities are made possible because of this parser. Currently, it cannot parse a full SQL. * @module @@ -133,6 +135,7 @@ function parseValue(value) { * @returns {Object[]} */ function parseExprList(str, ...values) { + if (str instanceof Raw) return [ str ]; let i = 0; let chr = str[i]; let valueIndex = 0; diff --git a/src/query_object.js b/src/query_object.js index ed38ad87..0e08de70 100644 --- a/src/query_object.js +++ b/src/query_object.js @@ -3,7 +3,7 @@ const util = require('util'); const { isPlainObject } = require('./utils'); const { parseExpr } = require('./expr'); -const Raw = require('./raw'); +const Raw = require('./raw').default; // deferred to break cyclic dependencies let Spell; diff --git a/src/raw.js b/src/raw.js deleted file mode 100644 index bb6bc659..00000000 --- a/src/raw.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -module.exports = class Raw { - constructor(value) { - this.value = value; - // consumed in expr_formatter.js - this.type = 'raw'; - } -}; diff --git a/src/raw.ts b/src/raw.ts new file mode 100644 index 00000000..9611f0f7 --- /dev/null +++ b/src/raw.ts @@ -0,0 +1,21 @@ +export default class Raw { + value: string; + + // consumed in expr_formatter.js + type: string = 'raw'; + + constructor(value: string) { + if (typeof value !== 'string') { + throw new Error('invalid type of raw value'); + } + this.value = value; + } + + toString() { + return this.value; + } + + static build(value: string) { + return new Raw(value); + } +}; diff --git a/src/realm.js b/src/realm.js index 23997ba0..e1ea1faf 100644 --- a/src/realm.js +++ b/src/realm.js @@ -8,7 +8,7 @@ const { findDriver, AbstractDriver } = require('./drivers'); const { camelCase } = require('./utils/string'); const { isBone } = require('./utils'); const sequelize = require('./adapters/sequelize'); -const Raw = require('./raw'); +const Raw = require('./raw').default; const { LEGACY_TIMESTAMP_MAP } = require('./constants'); const SequelizeBone = sequelize(Bone); diff --git a/src/spell.d.ts b/src/spell.d.ts index a0d2568b..a307643e 100644 --- a/src/spell.d.ts +++ b/src/spell.d.ts @@ -2,7 +2,7 @@ import { Literal, command, Raw, Connection, ResultSet, QueryResult, QueryOptions, SetOptions, WithOptions, - Collection, WhereConditions, OrderOptions, + Collection, WhereConditions, OrderOptions, BoneColumns, } from './types/common'; import { AbstractBone } from './types/abstract_bone'; import { Hint, IndexHint, CommonHintsArgs, HintInterface } from './hint'; @@ -134,15 +134,28 @@ export class Spell | Collecti $limit(rowCount: number): Spell; limit(rowCount: number): Spell; - count(name?: string): Spell | number>>; - average(name?: string): Spell | number>>; - minimum(name?: string): Spell | number>>; - maximum(name?: string): Spell | number>>; - sum(name?: string): Spell | number>>; + // aggregator(name: string) for Model.first/all/last.aggregator(name) because of ts(2526) + count(name?: BoneColumns): Spell | number>>; + count(name?: Raw | string): Spell | number>>; + + average(name?: BoneColumns): Spell | number>>; + average(name?: Raw | string): Spell | number>>; + + minimum(name?: BoneColumns): Spell | number>>; + minimum(name?: Raw | string): Spell | number>>; + + maximum(name?: BoneColumns): Spell | number>>; + maximum(name?: Raw | string): Spell | number>>; + + sum(name?: BoneColumns): Spell | number>>; + sum(name?: Raw | string): Spell | number>>; batch(size?: number): AsyncIterable; + increment(name: BoneColumns, by?: number, options?: QueryOptions): Spell; increment(name: string, by?: number, options?: QueryOptions): Spell; + + decrement(name: BoneColumns, by?: number, options?: QueryOptions): Spell; decrement(name: string, by?: number, options?: QueryOptions): Spell; toSqlString(): string; diff --git a/src/spell.js b/src/spell.js index c2beeabc..69e020d5 100644 --- a/src/spell.js +++ b/src/spell.js @@ -10,7 +10,7 @@ const { parseExprList, parseExpr, walkExpr } = require('./expr'); const { isPlainObject } = require('./utils'); const { IndexHint, INDEX_HINT_TYPE, Hint } = require('./hint'); const { parseObject } = require('./query_object'); -const Raw = require('./raw'); +const Raw = require('./raw').default; const { AGGREGATOR_MAP } = require('./constants'); /** @@ -961,7 +961,11 @@ for (const aggregator in AGGREGATOR_MAP) { configurable: true, writable: true, value: function Spell_aggregator(name = '*') { - if (name != '*' && parseExpr(name).type != 'id') { + if (name instanceof Raw) { + this.$select(Raw.build(`${func.toUpperCase()}(${name}) AS ${aggregator}`)); + return this + } + if (name !== '*' && parseExpr(name).type !== 'id') { throw new Error(`unexpected operand ${name} for ${func.toUpperCase()}()`); } this.$select(`${func}(${name}) as ${aggregator}`); diff --git a/src/types/abstract_bone.d.ts b/src/types/abstract_bone.d.ts index eec3c321..1a5449a6 100644 --- a/src/types/abstract_bone.d.ts +++ b/src/types/abstract_bone.d.ts @@ -3,10 +3,11 @@ import { Pool, Literal, WhereConditions, Collection, ResultSet, OrderOptions, QueryOptions, AttributeMeta, AssociateOptions, Values, Connection, BulkCreateOptions, - GeneratorReturnType, + GeneratorReturnType, BoneColumns, InstanceColumns, Raw, } from './common'; import { AbstractDriver } from '../drivers'; import { Spell } from '../spell'; +import { Key } from "readline"; interface SyncOptions { force?: boolean; @@ -17,7 +18,10 @@ interface TransactionOptions { connection: Connection; } +type V = Record; export class AbstractBone { + static name: string; + static DataTypes: typeof DataTypes; /** @@ -60,6 +64,11 @@ export class AbstractBone { */ static attributes: { [key: string]: AbstractDataType | AttributeMeta }; + /** + * The actual attribute definitions of the model. + */ + static columnAttributes: { [key: string]: AbstractDataType | AttributeMeta }; + /** * The schema info of current model. */ @@ -89,11 +98,18 @@ export class AbstractBone { * @example * Bone.renameAttribute('foo', 'bar') */ - static renameAttribute(originalName: string, newName: string): void; + static renameAttribute(this: T, originalName: BoneColumns, newName: string): void; + // after renamed + static renameAttribute(this: T, originalName: string, newName: string): void; - static alias(name: string): string; - static alias(data: Record): Record; - static unalias(name: string): string; + static alias(this: T, name: BoneColumns): string; + static alias(this: T, data: Record, Literal>): Record; + static unalias(this: T, name: BoneColumns): string; + + // after alias/unalias + static alias(this: T, name: string): string; + static alias(this: T, data: Record): Record; + static unalias(this: T, name: string): string; static hasOne(name: string, opts?: AssociateOptions): void; static hasMany(name: string, opts?: AssociateOptions): void; @@ -104,7 +120,7 @@ export class AbstractBone { * @example * Bone.create({ foo: 1, bar: 'baz' }) */ - static create(this: T, values: Values> & Record, options?: QueryOptions): Promise>; + static create(this: T, values: Values> & Partial, Literal>>, options?: QueryOptions): Promise>; /** * INSERT or UPDATE rows @@ -113,12 +129,12 @@ export class AbstractBone { * @param values values * @param opt query options */ - static upsert(this: T, values: Object, options?: QueryOptions): Spell; + static upsert(this: T, values: Partial, Literal>>, options?: QueryOptions): Spell; /** * Batch INSERT */ - static bulkCreate(this: T, records: Array>, options?: BulkCreateOptions): Promise>>; + static bulkCreate(this: T, records: Array, Literal>>>, options?: BulkCreateOptions): Promise>>; /** * SELECT all rows. In production, when the table is at large, it is not recommended to access records in this way. To iterate over all records, {@link Bone.batch} shall be considered as the better alternative. For tables with soft delete enabled, which means they've got `deletedAt` attribute, use {@link Bone.unscoped} to discard the default scope. @@ -159,6 +175,7 @@ export class AbstractBone { * Bone.select('MONTH(date), foo + 1') * Bone.select(name => name !== foo) */ + static select(this: T, ...names: Array> | string[]): Spell; static select(this: T, ...names: string[]): Spell; static select(this: T, filter: (name: string) => boolean): Spell; @@ -167,8 +184,8 @@ export class AbstractBone { * @example * Bone.join(Muscle, 'bones.id == muscles.boneId') */ - static join(this: T, Model: AbstractBone, onConditions: string, ...values: Literal[]): Spell>>; static join(this: T, Model: AbstractBone, onConditions: WhereConditions): Spell>>; + static join(this: T, Model: AbstractBone, onConditions: string, ...values: Literal[]): Spell>>; /** * Set WHERE conditions @@ -176,8 +193,8 @@ export class AbstractBone { * Bone.where('foo = ?', 1) * Bone.where({ foo: { $eq: 1 } }) */ - static where(this: T, whereConditions: string, ...values: Literal[]): Spell>>; static where(this: T, whereConditions: WhereConditions): Spell>>; + static where(this: T, whereConditions: string, ...values: Literal[]): Spell>>; /** * Set GROUP fields @@ -185,7 +202,8 @@ export class AbstractBone { * Bone.group('foo') * Bone.group('MONTH(createdAt)') */ - static group(this: T, ...names: string[]): Spell>; + static group(this: T, ...names: Array>): Spell>; + static group(this: T, ...names: Array): Spell>; /** * Set ORDER fields @@ -194,14 +212,17 @@ export class AbstractBone { * Bone.order('foo', 'desc') * Bone.order({ foo: 'desc' }) */ - static order(this: T, name: string, order?: 'desc' | 'asc'): Spell; + static order(this: T, name: BoneColumns, order?: 'desc' | 'asc'): Spell; static order(this: T, opts: OrderOptions): Spell; - static count(this: T, name?: string): Spell | number>; - static average(this: T, name?: string): Spell | number>; - static minimum(this: T, name?: string): Spell | number>; - static maximum(this: T, name?: string): Spell | number>; - + static count(this: T, name?: BoneColumns): Spell | number>; + static count(this: T, name?: Raw): Spell | number>; + static average(this: T, name?: BoneColumns): Spell | number>; + static average(this: T, name?: Raw): Spell | number>; + static minimum(this: T, name?: BoneColumns): Spell | number>; + static minimum(this: T, name?: Raw): Spell | number>; + static maximum(this: T, name?: BoneColumns): Spell | number>; + static maximum(this: T, name?: Raw): Spell | number>; /** * Remove rows. If soft delete is applied, an UPDATE query is performed instead of DELETing records directly. Set `forceDelete` to true to force a `DELETE` query. */ @@ -241,7 +262,10 @@ export class AbstractBone { * bone.attribute('foo'); // => 1 * bone.attribute('foo', 2); // => bone */ + attribute>(this: T, name: Key, value: Literal): void; attribute(this: T, name: Key, value: Literal): void; + + attribute, U extends T[Key]>(this: T, name: Key): U extends Literal ? U : Literal; attribute(this: T, name: Key): U extends Literal ? U : Literal; /** @@ -250,6 +274,7 @@ export class AbstractBone { * bone.attributeWas('foo') // => 1 */ attributeWas, U extends T[Key]>(this: T, key: Key): U extends Literal ? U : Literal; + attributeWas(this: T, key: Key): U extends Literal ? U : Literal; /** * See if attribute has been changed or not. @@ -257,28 +282,42 @@ export class AbstractBone { * @example * bone.attributeChanged('foo') */ - attributeChanged(name: string): boolean; + attributeChanged>(this: T, name: Key): boolean; + // for getter/setter + attributeChanged(this: T, name: Key): boolean; /** * Get changed attributes or check if given attribute is changed or not */ - changed(name: string): boolean; - changed(): Array | false; + changed>(this: T, name: Key): boolean; + // for getter/setter + changed(this: T, name: Key): boolean; + + changed>(this: T): Array | false; + // for getter/setter + changed(this: T): Array | false; /** * Get attribute changes */ - changes(name: string): Record; - changes(): Record; + changes>(this: T, name: Key): Record, [ Literal, Literal ]>; + changes(this: T, name: Key): Record, [ Literal, Literal ]>; + changes(): Record, [ Literal, Literal ]>; /** * See if attribute was changed previously or not. */ - previousChanged(name: string): boolean; - previousChanged(): Array; + previousChanged>(this: T, name: Key): boolean; + previousChanged(this: T, name: Key): boolean; + + previousChanged>(this: T): Array; + previousChanged(this: T): Array; + + previousChanges>(this: T, name: Key): boolean; + previousChanges(this: T, name: Key): boolean; - previousChanges(name: string): boolean; - previousChanges(): Array; + previousChanges>(this: T ): Array; + previousChanges(this: T ): Array; /** * Persist changes of current record to database. If current record has never been saved before, an INSERT query is performed. If the primary key was set and is not changed since, an UPDATE query is performed. If the primary key is changed, an INSERT ... UPDATE query is performed instead. diff --git a/src/types/common.d.ts b/src/types/common.d.ts index e51818c1..9d30a6d3 100644 --- a/src/types/common.d.ts +++ b/src/types/common.d.ts @@ -146,19 +146,21 @@ export class Raw { type: 'raw'; } -export type SetOptions = { - [key: string]: Literal -} | { +export type SetOptions = { [Property in keyof Extract, Literal>]: Literal +} | { + [key: string]: Literal }; export type WithOptions = { [qualifier: string]: { select: string | string[], throughRelation?: string } } -type OrderOptions = { - [Property in keyof Extract, Literal>]: 'desc' | 'asc' -} | { [name: string]: 'desc' | 'asc' } | Array | string | Raw; +type OrderOptions = { + [key in keyof Extract, Literal>]?: 'desc' | 'asc' +} | [ BoneColumns, 'desc' | 'asc' ] +| Array | [ BoneColumns, 'desc' | 'asc' ] | Raw | string> +| string | Raw; export class Collection extends Array { save(): Promise; @@ -177,6 +179,10 @@ export type PickTypeKeys = ({ [P in export type Values = Partial | 'isNewRecord' | 'Model' | 'dataValues'>>; +export type BoneColumns = keyof Values>> = Key; + +export type InstanceColumns> = Key; + export type BeforeHooksType = 'beforeCreate' | 'beforeBulkCreate' | 'beforeUpdate' | 'beforeSave' | 'beforeUpsert' | 'beforeRemove'; export type AfterHooksType = 'afterCreate' | 'afterBulkCreate' | 'afterUpdate' | 'afterSave' | 'afterUpsert' | 'afterRemove'; diff --git a/test/integration/custom.test.js b/test/integration/custom.test.js index d4fdc214..43cf9fac 100644 --- a/test/integration/custom.test.js +++ b/test/integration/custom.test.js @@ -9,7 +9,7 @@ const { connect, raw, Bone, disconnect } = require('../..'); const { checkDefinitions } = require('./helpers'); const { formatConditions, collectLiteral } = require('../../src/expr_formatter'); const { findExpr } = require('../../src/expr'); -const Raw = require('../../src/raw'); +const Raw = require('../../src/raw').default; const SqliteDriver = require('../../src/drivers/sqlite'); diff --git a/test/types/basics.test.ts b/test/types/basics.test.ts index 0ee9f9a9..baca01ec 100644 --- a/test/types/basics.test.ts +++ b/test/types/basics.test.ts @@ -1,5 +1,6 @@ import { strict as assert } from 'assert'; -import Realm, { Bone, Column, DataTypes, connect } from '../..'; +import sinon from 'sinon'; +import Realm, { Bone, Column, DataTypes, connect, Raw } from '../..'; describe('=> Basics (TypeScript)', function() { const { TEXT } = DataTypes; @@ -71,6 +72,11 @@ describe('=> Basics (TypeScript)', function() { return this.wordCount <= 0; } + @Column(DataTypes.VIRTUAL) + get shouldBeRemove(): boolean { + return this.wordCount <= 0; + } + nodes: unknown; } @@ -91,6 +97,15 @@ describe('=> Basics (TypeScript)', function() { }); describe('=> Attributes', function() { + it('Bone.renameAttribute', () => { + Post.renameAttribute('shouldBeRemove', 'newName'); + assert.ok(Post.attributes['newName']); + assert.ok(!Post.attributes['shouldBeRemove']); + Post.renameAttribute('newName', 'shouldBeRemove'); + assert.ok(Post.attributes['shouldBeRemove']); + assert.ok(!Post.attributes['newName']); + }); + it('bone.attribute(name)', async function() { const post = await Post.create({ title: 'Cain' }); assert.equal(post.attribute('title'), 'Cain'); @@ -151,7 +166,44 @@ describe('=> Basics (TypeScript)', function() { assert.equal(post.changed('title'), false); }); - it('bone.attributeWas(name)',async () => { + it('bone.previousChanged()', async function() { + const post = new Post({ title: 'Cain' }); + assert.deepEqual(post.previousChanged(), false); + assert.equal(post.previousChanged('title'), false); + + await post.create(); + + assert.ok(post.id); + assert.equal(post.title, 'Cain'); + assert.deepEqual(post.previousChanged().sort(), [ 'id', 'title', 'createdAt', 'updatedAt' ].sort()); + assert.equal(post.previousChanged('title'), true); + }); + + it('bone.changes()', async function() { + const post = new Post({ title: 'Cain' }); + assert.deepEqual(post.changes(), { title: [ null, 'Cain' ] }); + assert.deepEqual(post.changes('title'), { title: [ null, 'Cain' ] }); + + await post.create(); + + assert.ok(post.id); + assert.equal(post.title, 'Cain'); + assert.deepEqual(post.changes('title'), {}); + }); + + it('bone.previousChanges()', async function() { + const post = new Post({ title: 'Cain' }); + assert.deepEqual(post.previousChanges(), {}); + assert.deepEqual(post.previousChanges('title'), {}); + + await post.create(); + + assert.ok(post.id); + assert.equal(post.title, 'Cain'); + assert.deepEqual(post.changes('title'), {}); + }); + + it('bone.attributeWas(name)', async () => { const post = new Post({ title: 'Yhorm', }); @@ -159,6 +211,16 @@ describe('=> Basics (TypeScript)', function() { post.attribute('title', 'Cain'); assert.equal(post.attributeWas('title'), 'Yhorm'); }); + + it('bone.attributeChanged',async () => { + const post = new Post({ + title: 'Yhorm', + }); + await post.save(); + assert.equal(post.attributeChanged('title'), false); + }); + + }); describe('=> Accessors', function() { @@ -401,4 +463,51 @@ describe('=> Basics (TypeScript)', function() { assert.equal(result, 1); }); }); + + describe('Num', () => { + + let clock; + before(() => { + const date = new Date(2017, 11, 12); + const fakeDate = date.getTime(); + sinon.useFakeTimers(fakeDate); + }); + + after(() => { + clock?.restore(); + }); + + it('count', () => { + assert.equal(Post.count('authorId').toSqlString(), 'SELECT COUNT("author_id") AS "count" FROM "articles" WHERE "gmt_deleted" IS NULL'); + assert.equal(Post.count(new Raw("DISTINCT(author_id)")).toSqlString(), 'SELECT COUNT(DISTINCT(author_id)) AS count FROM "articles" WHERE "gmt_deleted" IS NULL'); + }); + + it('average', () => { + assert.equal(Post.average('wordCount').toSqlString(), 'SELECT AVG("word_count") AS "average" FROM "articles" WHERE "gmt_deleted" IS NULL'); + assert.equal(Post.average(new Raw("DISTINCT(word_count)")).toSqlString(), 'SELECT AVG(DISTINCT(word_count)) AS average FROM "articles" WHERE "gmt_deleted" IS NULL'); + }); + + it('minimum', () => { + assert.equal(Post.minimum('wordCount').toSqlString(), 'SELECT MIN("word_count") AS "minimum" FROM "articles" WHERE "gmt_deleted" IS NULL'); + assert.equal(Post.minimum(new Raw("DISTINCT(word_count)")).toSqlString(), 'SELECT MIN(DISTINCT(word_count)) AS minimum FROM "articles" WHERE "gmt_deleted" IS NULL'); + }); + + it('maximum', () => { + assert.equal(Post.maximum('wordCount').toSqlString(), 'SELECT MAX("word_count") AS "maximum" FROM "articles" WHERE "gmt_deleted" IS NULL'); + assert.equal(Post.maximum(new Raw("DISTINCT(word_count)")).toSqlString(), 'SELECT MAX(DISTINCT(word_count)) AS maximum FROM "articles" WHERE "gmt_deleted" IS NULL'); + }); + + it('sum', () => { + assert.equal(Post.sum('wordCount').toSqlString(), 'SELECT SUM("word_count") AS "sum" FROM "articles" WHERE "gmt_deleted" IS NULL'); + assert.equal(Post.sum(new Raw("DISTINCT(word_count)")).toSqlString(), 'SELECT SUM(DISTINCT(word_count)) AS sum FROM "articles" WHERE "gmt_deleted" IS NULL'); + }); + + it('increment', () => { + assert.equal(Post.find().increment('wordCount').toSqlString(), 'UPDATE "articles" SET "word_count" = "word_count" + 1, "gmt_modified" = \'2017-12-12 00:00:00.000\' WHERE "gmt_deleted" IS NULL'); + }); + + it('decrement', () => { + assert.equal(Post.find().decrement('wordCount').toSqlString(), 'UPDATE "articles" SET "word_count" = "word_count" - 1, "gmt_modified" = \'2017-12-12 00:00:00.000\' WHERE "gmt_deleted" IS NULL'); + }); + }); }); diff --git a/test/types/custom_driver.test.ts b/test/types/custom_driver.test.ts index 3b658a71..c10f4da6 100644 --- a/test/types/custom_driver.test.ts +++ b/test/types/custom_driver.test.ts @@ -1,10 +1,9 @@ import { strict as assert } from 'assert'; const SqlString = require('sqlstring'); -import Realm, { SqliteDriver, SpellMeta, Literal, SpellBookFormatResult, Column } from '../..'; +import Realm, { SqliteDriver, SpellMeta, Literal, SpellBookFormatResult, Column, Raw } from '../..'; const { formatConditions, collectLiteral } = require('../../src/expr_formatter'); const { findExpr } = require('../../src/expr'); -const Raw = require('../../src/raw'); interface FormatResult { table?: string; diff --git a/test/types/querying.test.ts b/test/types/querying.test.ts index a45068d8..4ca5bc4f 100644 --- a/test/types/querying.test.ts +++ b/test/types/querying.test.ts @@ -87,7 +87,9 @@ describe('=> Querying (TypeScript)', function() { describe('=> Aggregations', function() { it('Bone.count()', async function() { - const count = await Post.count(); + let count = await Post.count(); + assert.equal(count, 0); + count = await Post.count('authorId'); assert.equal(count, 0); }); @@ -102,7 +104,9 @@ describe('=> Querying (TypeScript)', function() { }); it('Bone.where().count()', async function() { - const count = await Post.where({ title: 'Leah' }).count(); + let count = await Post.where({ title: 'Leah' }).count(); + assert.equal(count, 0); + count = await Post.where({ title: 'Leah' }).count('id'); assert.equal(count, 0); }); }); diff --git a/test/types/sequelize.test.ts b/test/types/sequelize.test.ts index 645ee6fc..79b7f37a 100644 --- a/test/types/sequelize.test.ts +++ b/test/types/sequelize.test.ts @@ -4,7 +4,7 @@ const sinon = require('sinon'); import { SequelizeBone, Column, DataTypes, connect, Hint, Raw, Bone } from '../..'; describe('=> sequelize (TypeScript)', function() { - const { TEXT, STRING } = DataTypes; + const { TEXT, STRING, VIRTUAL } = DataTypes; class Post extends SequelizeBone { static table = 'articles'; @@ -50,6 +50,15 @@ describe('=> sequelize (TypeScript)', function() { defaultValue: 0, }) wordCount: number; + + @Column(VIRTUAL) + get virtualField() { + return this.getDataValue('content')?.toLowerCase(); + } + + set virtualField(v: string) { + this.setDataValue('content', v?.toUpperCase()); + } } class Book extends SequelizeBone { @@ -74,7 +83,10 @@ describe('=> sequelize (TypeScript)', function() { price: number; } - class Like extends SequelizeBone {} + class Like extends SequelizeBone { + @Column() + userId: number; + } before(async function() { Bone.driver = null; @@ -176,6 +188,7 @@ describe('=> sequelize (TypeScript)', function() { settings: null, summary: null, thumb: null, + virtualField: null, }); }); @@ -335,6 +348,7 @@ describe('=> sequelize (TypeScript)', function() { order: 'createdAt desc, id desc', limit: 1, }); + assert.equal(posts.length, 1); assert.equal(posts[0].title, 'Tyrael'); @@ -1039,6 +1053,14 @@ describe('=> sequelize (TypeScript)', function() { ]); assert.equal(await Post.count(), 2); }); + + it('Model.count(name)', async () => { + await Promise.all([ + Post.create({ title: 'By three they come' }), + Post.create({ title: 'By three thy way opens' }), + ]); + assert.equal(await Post.count('title'), 2); + }); it('Model.count({ paranoid: false })', async () => { await Promise.all([ @@ -1160,6 +1182,7 @@ describe('=> sequelize (TypeScript)', function() { await Book.create({ name: 'Book of Tyrael', price: 20 }), await Book.create({ name: 'Book of Cain', price: 10 }), ]); + Post.find().decrement('authorId') const min = await Book.min('price', { where: { name: 'Book of Tyrael' }, }); diff --git a/test/types/spell.test.ts b/test/types/spell.test.ts index 57bc5be9..9027c21f 100644 --- a/test/types/spell.test.ts +++ b/test/types/spell.test.ts @@ -1,8 +1,10 @@ import { strict as assert } from 'assert'; +import sinon from 'sinon'; + import { Bone, DataTypes, Column, connect, INDEX_HINT_SCOPE_TYPE, - INDEX_HINT_TYPE, INDEX_HINT_SCOPE, Hint, IndexHint + INDEX_HINT_TYPE, INDEX_HINT_SCOPE, Hint, IndexHint, Raw } from '../..'; describe('=> Spell (TypeScript)', function() { @@ -168,5 +170,51 @@ describe('=> Spell (TypeScript)', function() { 'UPDATE /*+ idx_title idx_user_id idx_halo */ `articles` USE INDEX (idx_is) USE INDEX FOR ORDER BY (idx_hello) IGNORE INDEX FOR ORDER BY (idx_haw) SET `title` = \'ssss\' WHERE `id` = 1 AND `gmt_deleted` IS NULL ORDER BY `author_id`' ); }); - }) + }); + + describe('Num', () => { + let clock; + before(() => { + const date = new Date(2017, 11, 12); + const fakeDate = date.getTime(); + sinon.useFakeTimers(fakeDate); + }); + + after(() => { + clock?.restore(); + }); + + it('count', () => { + assert.equal(Post.all.count('authorId').toSqlString(), 'SELECT COUNT(`author_id`) AS `count` FROM `articles` WHERE `gmt_deleted` IS NULL'); + assert.equal(Post.all.count(new Raw(`DISTINCT(author_id)`)).toSqlString(), 'SELECT COUNT(DISTINCT(author_id)) AS count FROM `articles` WHERE `gmt_deleted` IS NULL'); + }); + + it('average', () => { + assert.equal(Post.all.average('word_count').toSqlString(), 'SELECT AVG(`word_count`) AS `average` FROM `articles` WHERE `gmt_deleted` IS NULL'); + assert.equal(Post.all.average(new Raw(`DISTINCT(word_count)`)).toSqlString(), 'SELECT AVG(DISTINCT(word_count)) AS average FROM `articles` WHERE `gmt_deleted` IS NULL'); + }); + + it('minimum', () => { + assert.equal(Post.all.minimum('word_count').toSqlString(), 'SELECT MIN(`word_count`) AS `minimum` FROM `articles` WHERE `gmt_deleted` IS NULL'); + assert.equal(Post.all.minimum(new Raw(`DISTINCT(word_count)`)).toSqlString(), 'SELECT MIN(DISTINCT(word_count)) AS minimum FROM `articles` WHERE `gmt_deleted` IS NULL'); + }); + + it('maximum', () => { + assert.equal(Post.all.maximum('word_count').toSqlString(), 'SELECT MAX(`word_count`) AS `maximum` FROM `articles` WHERE `gmt_deleted` IS NULL'); + assert.equal(Post.all.maximum(new Raw(`DISTINCT(word_count)`)).toSqlString(), 'SELECT MAX(DISTINCT(word_count)) AS maximum FROM `articles` WHERE `gmt_deleted` IS NULL'); + }); + + it('sum', () => { + assert.equal(Post.all.sum('word_count').toSqlString(), 'SELECT SUM(`word_count`) AS `sum` FROM `articles` WHERE `gmt_deleted` IS NULL'); + assert.equal(Post.all.sum(new Raw(`DISTINCT(word_count)`)).toSqlString(), 'SELECT SUM(DISTINCT(word_count)) AS sum FROM `articles` WHERE `gmt_deleted` IS NULL'); + }); + + it('increment', () => { + assert.equal(Post.all.increment('word_count').toSqlString(), 'UPDATE `articles` SET `word_count` = `word_count` + 1, `gmt_modified` = \'2017-12-12 00:00:00.000\' WHERE `gmt_deleted` IS NULL'); + }); + + it('decrement', () => { + assert.equal(Post.all.decrement('word_count').toSqlString(), 'UPDATE `articles` SET `word_count` = `word_count` - 1, `gmt_modified` = \'2017-12-12 00:00:00.000\' WHERE `gmt_deleted` IS NULL'); + }); + }); }); diff --git a/test/unit/data_types.test.js b/test/unit/data_types.test.js index 06beea15..f36400e4 100644 --- a/test/unit/data_types.test.js +++ b/test/unit/data_types.test.js @@ -3,7 +3,7 @@ const assert = require('assert').strict; const dayjs = require('dayjs'); const { default: DataTypes } = require('../../src/data_types'); -const Raw = require('../../src/raw'); +const Raw = require('../../src/raw').default; const Postgres_DataTypes = require('../../src/drivers/postgres/data_types'); const SQLite_DataTypes = require('../../src/drivers/sqlite/data_types');