Skip to content

Commit

Permalink
feat: export more ts type definitions and use deep-equal module (#306)
Browse files Browse the repository at this point in the history
* fix: use deep-equal

* feat: add type definitions

* fix: type definitions

* fix: type definition unit test

* fix: SpellBookFormatResult

Co-authored-by: JimmyDaddy <[email protected]>
  • Loading branch information
JimmyDaddy and JimmyDaddy authored Apr 23, 2022
1 parent 205dad7 commit 18907a7
Show file tree
Hide file tree
Showing 7 changed files with 601 additions and 26 deletions.
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +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 { MysqlDriver, PostgresDriver, SqliteDriver, AbstractDriver } = require('./src/drivers');

/**
Expand Down Expand Up @@ -55,6 +56,7 @@ Object.assign(Realm, {
PostgresDriver,
SqliteDriver,
AbstractDriver,
Raw,
});

module.exports = Realm;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
},
"dependencies": {
"debug": "^3.1.0",
"deep-equal": "^2.0.5",
"heredoc": "^1.3.1",
"pluralize": "^7.0.0",
"reflect-metadata": "^0.1.13",
Expand Down
11 changes: 6 additions & 5 deletions src/bone.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* @module
*/
const util = require('util');
const deepEqual = require('deep-equal');
const pluralize = require('pluralize');
const { executeValidator, LeoricValidateError } = require('./validator');
require('reflect-metadata');
Expand Down Expand Up @@ -380,7 +381,7 @@ class Bone {
if (this.#rawUnset.has(name) || !this.hasAttribute(name)) return false;
const value = this.attribute(name);
const valueWas = this.attributeWas(name);
return !util.isDeepStrictEqual(value, valueWas);
return !deepEqual(value, valueWas);
}

/**
Expand Down Expand Up @@ -412,15 +413,15 @@ class Bone {
if (this.#rawUnset.has(name) || this.#rawPrevious[name] === undefined || !this.hasAttribute(name)) return {};
const value = this.attribute(name);
const valueWas = this.#rawPrevious[name] == null ? null : this.#rawPrevious[name];
if (util.isDeepStrictEqual(value, valueWas)) return {};
if (deepEqual(value, valueWas)) return {};
return { [name]: [ valueWas, value ] };
}
const result = {};
for (const attrKey of Object.keys(this.constructor.attributes)) {
if (this.#rawUnset.has(attrKey) || this.#rawPrevious[attrKey] === undefined) continue;
const value = this.attribute(attrKey);
const valueWas = this.#rawPrevious[attrKey] == null ? null : this.#rawPrevious[attrKey];
if (!util.isDeepStrictEqual(value, valueWas)) result[attrKey] = [ valueWas, value ];
if (!deepEqual(value, valueWas)) result[attrKey] = [ valueWas, value ];
}
return result;
}
Expand All @@ -439,7 +440,7 @@ class Bone {
if (this.#rawUnset.has(name) || !this.hasAttribute(name)) return {};
const value = this.attribute(name);
const valueWas = this.attributeWas(name);
if (util.isDeepStrictEqual(value, valueWas)) return {};
if (deepEqual(value, valueWas)) return {};
return { [name]: [ valueWas, value ] };
}
const result = {};
Expand All @@ -448,7 +449,7 @@ class Bone {
const value = this.attribute(attrKey);
const valueWas = this.attributeWas(attrKey);

if (!util.isDeepStrictEqual(value, valueWas)) {
if (!deepEqual(value, valueWas)) {
result[attrKey] = [ valueWas, value ];
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/hint.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const { isDeepStrictEqual, format } = require('util');
const { format } = require('util');
const isDeepStrictEqual = require('deep-equal');
const { isPlainObject } = require('./utils');

/**
Expand Down
248 changes: 248 additions & 0 deletions test/types/custom_driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import { strict as assert } from 'assert';
const SqlString = require('sqlstring');

import Realm, { SqliteDriver, SpellMeta, Literal, SpellBookFormatResult } from '../..';
const { formatConditions, collectLiteral } = require('../../src/expr_formatter');
const { findExpr } = require('../../src/expr');
const Raw = require('../../src/raw');

interface FormatResult {
table?: string;
whereArgs?: Array<Literal>
whereClause?: string,
values?: Array<Literal> | {
[key: string]: Literal
};
[key: string]: Literal
}

class MySpellbook extends SqliteDriver.Spellbook {

format(spell: SpellMeta): SpellBookFormatResult<FormatResult> {
for (const scope of spell.scopes) scope(spell);
switch (spell.command) {
case 'insert':
return this.formatMyInsert(spell);
case 'bulkInsert':
return this.formatInsert(spell);
case 'select':
return this.formatSelect(spell);
case 'update':
return this.formatUpdate(spell);
case 'delete':
return this.formatDelete(spell);
case 'upsert':
return this.formatUpsert(spell);
default:
throw new Error(`Unsupported SQL command ${spell.command}`);
}
}

formatUpdate(spell: SpellMeta): SpellBookFormatResult<FormatResult> {
const a = super.formatDelete(spell);

const { Model, sets, whereConditions } = spell;
const { shardingKey } = Model;
const { escapeId } = Model.driver;
if (shardingKey) {
if (sets.hasOwnProperty(shardingKey) && sets[shardingKey] == null) {
throw new Error(`Sharding key ${Model.table}.${shardingKey} cannot be NULL`);
}
if (!whereConditions.some(condition => findExpr(condition, { type: 'id', value: shardingKey }))) {
throw new Error(`Sharding key ${Model.table}.${shardingKey} is required.`);
}
}

if (Object.keys(sets).length === 0) {
throw new Error('Unable to update with empty set');
}

const table = escapeId(spell.table.value);
const opValues = {};
Object.keys(spell.sets).reduce((obj, key) => {
obj[escapeId(Model.unalias(key))] = spell.sets[key];
return obj;
}, opValues);

let whereArgs = [];
let whereClause = '';
if (whereConditions.length > 0) {
for (const condition of whereConditions) collectLiteral(spell, condition, whereArgs);
whereClause += `WHERE ${formatConditions(spell, whereConditions)}`;
}
return {
table,
whereArgs,
whereClause,
opValues,
};
}

formatDelete(spell: SpellMeta): SpellBookFormatResult<FormatResult> {
const { Model, whereConditions } = spell;
const { escapeId } = Model.driver;
const table = escapeId(spell.table.value);
let whereArgs = [];
let whereClause = '';
if (whereConditions.length > 0) {
for (const condition of whereConditions) collectLiteral(spell, condition, whereArgs);
whereClause += `WHERE ${formatConditions(spell, whereConditions)}`;
}
return {
table,
whereArgs,
whereClause,
};
}

formatMyInsert(spell: SpellMeta): SpellBookFormatResult<FormatResult> {
const { Model, sets } = spell;
const { escapeId } = Model.driver;
const table = escapeId(spell.table.value);
let values = {};

const { shardingKey } = Model;
if (shardingKey && sets[shardingKey] == null) {
throw new Error(`Sharding key ${Model.table}.${shardingKey} cannot be NULL.`);
}
for (const name in sets) {
const value = sets[name];
values[escapeId(Model.unalias(name))] = value instanceof Raw? SqlString.raw(value.value) : value;
}

return {
table,
values,
};
}

};

class CustomDriver extends SqliteDriver {
static Spellbook = MySpellbook;

async cast(spell) {
const { command } = spell;
switch (command) {
case 'update': {
const updateParams = this.format(spell);
return await this.update(updateParams, spell);
}
case 'delete': {
const deleteParams = this.format(spell);
return await this.delete(deleteParams, spell);
}
case 'insert': {
const insertParams = this.format(spell);
return await this.insert(insertParams, spell);
}
case 'upsert':
case 'bulkInsert':
case 'select': {
const { sql, values } = this.format(spell);
const query = { sql, nestTables: command === 'select' };
return await this.query(query, values, spell);
}
default:
throw new Error('unspported sql command');
}
}

async update({ table, values, whereClause, whereArgs }, options?: SpellMeta) {
const valueSets = [];
const assignValues = [];
Object.keys(values).map((key) => {
valueSets.push(`${key}=?`);
assignValues.push(values[key]);
});
const sql = `UPDATE ${table} SET ${valueSets.join(',')} ${whereClause}`;
return await this.query(sql, assignValues.concat(whereArgs), options);
}

async delete({ table, whereClause, whereArgs }, options) {
const sql = `DELETE FROM ${table} ${whereClause}`;
return await this.query(sql, whereArgs, options);
}

async insert({ table, values }: { table: string, values: {[key: string]: Literal}}, options?: SpellMeta) {
const valueSets = [];
const assignValues = [];
Object.keys(values).map((key) => {
valueSets.push(key);
assignValues.push(values[key]);
});
const sql = `INSERT INTO ${table} (${valueSets.join(',')}) VALUES (${valueSets.map(_ => '?')})`;
return await this.query(sql, assignValues, options);
}
};

describe('=> Realm (TypeScript)', function () {
let realm: Realm;
before(function() {
realm = new Realm({
driver: CustomDriver,
database: '/tmp/leoric.sqlite3',
subclass: true,
});
});

describe('realm.define(name, attributes, options, descriptors)', async function() {
it('options and descriptors should be optional', async function() {
assert.doesNotThrow(function() {
const { STRING } = realm.DataTypes;
realm.define('User', { name: STRING });
});
});

it('can customize attributes with descriptors', async function() {
const { STRING } = realm.DataTypes;
const User = realm.define('User', { name: STRING }, {}, {
get name() {
return this.attribute('name').replace(/^([a-z])/, function(m, chr) {
return chr.toUpperCase();
});
},
set name(value) {
if (typeof value !== 'string') throw new Error('unexpected name' + value);
this.attribute('name', value);
}
});
// User.findOne should exists
assert(User.findOne);
});
});

describe('realm.sync(options)', async function() {
it('options should be optional', async function() {
assert.doesNotThrow(async () => {
const { STRING } = realm.DataTypes;
realm.define('User', { name: STRING });
await realm.sync();
});
});

it('`force` can be passed individually', async function() {
assert.doesNotThrow(async () => {
const { STRING } = realm.DataTypes;
realm.define('User', { name: STRING });
await realm.sync({ force: true });
});
});

it('`alter` can be passed individually', async function() {
assert.doesNotThrow(async () => {
const { STRING } = realm.DataTypes;
realm.define('User', { name: STRING });
await realm.sync({ alter: true });
});
});

it('`force` and `alter` can be passed together', async function() {
assert.doesNotThrow(async () => {
const { STRING } = realm.DataTypes;
realm.define('User', { name: STRING });
await realm.sync({ force: true, alter: true });
});
});
});
});
Loading

0 comments on commit 18907a7

Please sign in to comment.