From ea096fc03638703d6991587a85422c61f61209bc Mon Sep 17 00:00:00 2001 From: Olavi Mustanoja Date: Wed, 19 Feb 2020 02:25:30 +0200 Subject: [PATCH] Add type declarations (resolve #46) --- index.d.ts | 39 ++++++++++++ index.test.ts | 169 ++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 9 ++- tsconfig.json | 16 +++++ tslint.json | 31 +++++++++ yarn.lock | 30 ++++++++- 6 files changed, 288 insertions(+), 6 deletions(-) create mode 100644 index.d.ts create mode 100644 index.test.ts create mode 100644 tsconfig.json create mode 100644 tslint.json diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..fa9d2b0 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,39 @@ +import { Model, ModelClass, Page } from 'objection-2'; +import { QueryBuilder } from 'knex'; + +export as namespace ObjectionHashID; + +export interface HashProperties { + hashid: string; + hashId: string; +} + +export class AuthQueryBuilder { + ArrayQueryBuilderType: AuthQueryBuilder; + SingleQueryBuilderType: AuthQueryBuilder; + NumberQueryBuilderType: AuthQueryBuilder; + PageQueryBuilderType: AuthQueryBuilder>; + + findByHashId: (hashId: string) => this['SingleQueryBuilderType'] & M['QueryBuilderType']['SingleQueryBuilderType']; +} + +export interface HashIdInstance { + QueryBuilderType: AuthQueryBuilder; + + hashid: string; + hashId: string; +} + +export interface HashIdStatic { + QueryBuilder: typeof AuthQueryBuilder; + hashIdSalt: string; + hashIdMinLength: number | void; + hashIdAlphabet: string | void; + hashIdSeps: string | void; + hashIdField: string | boolean; + hashedFields: Array; + + new (): HashIdInstance & T['prototype']; +} + +export default function hashid(model: T): HashIdStatic & Omit & T['prototype']; \ No newline at end of file diff --git a/index.test.ts b/index.test.ts new file mode 100644 index 0000000..5b03186 --- /dev/null +++ b/index.test.ts @@ -0,0 +1,169 @@ +import plugin from '.'; +import {Model} from 'objection-2'; +import knexjs from 'knex'; + +const knex = knexjs({ + client: 'sqlite3', + connection: { filename: ':memory:' }, + useNullAsDefault: true +}); + +Model.knex(knex) + +class BaseModel extends plugin(Model) { + static get tableName () { + return 'table1' + } + + id!: string; +} + +class HiddenId extends BaseModel { + static hidden = ['id']; + static hashIdField = false; +} + +class AlgoliaObject extends BaseModel { + static get hashIdField () { + return 'ObjectID' + } +} + +class FatModel extends BaseModel { + static get hashedFields () { + return ['foo', 'bar'] + } +} + +class CompoundPK extends BaseModel { + static get tableName () { + return 'table2' + } + + static get idColumn () { + return ['x', 'y'] + } +} + +class SubModel extends BaseModel { + static get hashIdSalt () { + return 'static' + } + + static get hashIdMinLength () { + return 6 + } +} + +class ModelA extends SubModel { + static get relationMappings () { + return { + modelBs: { + relation: BaseModel.HasManyRelation, + modelClass: ModelB, + join: { + from: `${this.tableName}.id`, + to: `${ModelB.tableName}.fk_id` + } + } + } + } +} + +class ModelB extends SubModel { + static get tableName () { + return 'table3' + } + + static get hashedFields () { + return ['fk_id'] + } +} + +describe(`objection-hashid types (w/ objection v2)`, () => { + beforeAll(async () => { + await knex.schema.createTable(BaseModel.tableName, table => { + table.increments() + table.integer('foo') + }) + + await knex.schema.createTable(CompoundPK.tableName, table => { + table.integer('x').notNullable() + table.integer('y').notNullable() + table.primary(['x', 'y']) + }) + + await knex.schema.createTable(ModelB.tableName, table => { + table.increments() + table + .integer('fk_id') + .references(`${ModelA.tableName}.id`) + .notNullable() + }) + }) + + test('fills out hashId', async () => { + const model = await BaseModel.query().insert({}); + + expect(typeof model.id).toBe('number') + expect(typeof model.hashId).toBe('string') + expect(model.hashId.length).toBeGreaterThan(0) // hashid returns blank string on error + }) + + test('aliases hashid', async () => { + const model = await BaseModel.query().first() + + expect(model.hashid).toBeDefined() + }) + + test('writes hashid to resulting object', async () => { + const model = await BaseModel.query().first() + + expect(typeof model.toJSON().id).toEqual('string') + }) + + test('can change what field the hashed PK is written under', async () => { + const model = await AlgoliaObject.query().first() + + expect(typeof model.toJSON().ObjectID).toBe('string') + }) + + test('can hash other fields as well', async () => { + const model = await FatModel.query().insertAndFetch({ foo: 4 }) + + expect(typeof model.toJSON().foo).toBe('string') + }) + + let obj, hashId + + test('works with objection-visibility', async () => { + const model = await HiddenId.query().insertAndFetch({}) + obj = model.toJSON() + hashId = model.hashId + + expect(obj.id).toBeUndefined() + expect(typeof obj.hashId).toBe('undefined') + }) + + test('search by hashId', async () => { + const model = await HiddenId.query().findByHashId(hashId) + + expect(model).toBeTruthy() + }) + + test('works with compound primary keys', async () => { + const model = await CompoundPK.query().insertAndFetch({ x: 6, y: 9 }) + const hashId = model.toJSON().id + expect(typeof hashId).toBe('string') + + const instance = await CompoundPK.query().findByHashId(hashId) + expect(instance.$id()).toEqual([6, 9]) + }) + + test('maintains reference across multiple models', async () => { + const modelA = await ModelA.query().insertAndFetch({}) + const modelB = await modelA.$relatedQuery('modelBs').insert({}) + + expect(modelA.toJSON().id).toEqual(modelB.toJSON().fk_id) + }) +}) \ No newline at end of file diff --git a/package.json b/package.json index 21d9622..944b5a5 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "author": "Jane Jeon ", "license": "MIT", "scripts": { - "test": "jest", + "test": "jest index.test.js", "test:watch": "yarn test --watch", "test:ci": "yarn test --ci --reporters=default --reporters=jest-junit", "lint": "standard | snazzy && prettier --check '**/*.{md,json,yml,yaml}'" @@ -37,7 +37,9 @@ "lodash.memoize": "^4.1.2" }, "devDependencies": { - "@types/jest": "^25.1.1", + "@types/jest": "^25.1.2", + "@types/mocha": "^7.0.1", + "@types/node": "^13.7.2", "husky": "^4.2.1", "jest": "^25.1.0", "jest-junit": "^10.0.0", @@ -49,7 +51,8 @@ "prettier": "^1.18.2", "snazzy": "^8.0.0", "sqlite3": "^4.1.0", - "standard": "^14.1.0" + "standard": "^14.1.0", + "tslint-no-unused-expression-chai": "^0.1.4" }, "peerDependencies": { "objection": "^1 || ^2" diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0fa9a52 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "lib": ["es6", "dom"], + "esModuleInterop": true, + "noImplicitThis": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "types": ["mocha", "jest"], + + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "files": ["index.d.ts", "index.test.ts"] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..840ffd8 --- /dev/null +++ b/tslint.json @@ -0,0 +1,31 @@ +{ + "defaultSeverity": "error", + "extends": ["tslint:recommended", "tslint-no-unused-expression-chai"], + "jsRules": {}, + "rules": { + "indent": [true, "tabs"], + "quotemark": [true, "single"], + "arrow-parens": [true, "ban-single-arg-parens"], + "whitespace": false, + "ordered-imports": false, + "interface-name": [true, "never-prefix"], + "trailing-comma": [true, { "multiline": "never", "singleline": "never" }], + "semicolon": [true, "always", "ignore-bound-class-methods"], + "object-literal-sort-keys": false, + "object-literal-key-quotes": [true, "as-needed"], + "max-line-length": false, + "max-classes-per-file": false, + "only-arrow-functions": false, + "variable-name": [ + true, + "check-format", + "ban-keywords", + "allow-pascal-case", + "allow-leading-underscore" + ] + }, + "rulesDirectory": [], + "linterOptions": { + "exclude": ["node_modules"] + } +} diff --git a/yarn.lock b/yarn.lock index 70a3c91..ac97552 100644 --- a/yarn.lock +++ b/yarn.lock @@ -534,14 +534,24 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" -"@types/jest@^25.1.1": +"@types/jest@^25.1.2": version "25.1.2" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-25.1.2.tgz#1c4c8770c27906c7d8def5d2033df9dbd39f60da" + resolved "https://registry.npmjs.org/@types/jest/-/jest-25.1.2.tgz#1c4c8770c27906c7d8def5d2033df9dbd39f60da" integrity sha512-EsPIgEsonlXmYV7GzUqcvORsSS9Gqxw/OvkGwHfAdpjduNRxMlhsav0O5Kb0zijc/eXSO/uW6SJt9nwull8AUQ== dependencies: jest-diff "^25.1.0" pretty-format "^25.1.0" +"@types/mocha@^7.0.1": + version "7.0.1" + resolved "https://registry.npmjs.org/@types/mocha/-/mocha-7.0.1.tgz#5d7ec2a789a1f77c59b7ad071b9d50bf1abbfc9e" + integrity sha512-L/Nw/2e5KUaprNJoRA33oly+M8X8n0K+FwLTbYqwTcR14wdPWeRkigBLfSFpN/Asf9ENZTMZwLxjtjeYucAA4Q== + +"@types/node@^13.7.2": + version "13.7.2" + resolved "https://registry.npmjs.org/@types/node/-/node-13.7.2.tgz#50375b95b5845a34efda2ffb3a087c7becbc46c6" + integrity sha512-uvilvAQbdJvnSBFcKJ2td4016urcGvsiR+N4dHGU87ml8O2Vl6l+ErOi9w0kXSPiwJ1AYlIW+0pDXDWWMOiWbw== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -5105,11 +5115,25 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" -tslib@^1.9.0: +tslib@^1.8.1, tslib@^1.9.0: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== +tslint-no-unused-expression-chai@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/tslint-no-unused-expression-chai/-/tslint-no-unused-expression-chai-0.1.4.tgz#f4a2c9dd3306088f44eb7574cf470082b09ade49" + integrity sha512-frEWKNTcq7VsaWKgUxMDOB2N/cmQadVkUtUGIut+2K4nv/uFXPfgJyPjuNC/cHyfUVqIkHMAvHOCL+d/McU3nQ== + dependencies: + tsutils "^3.0.0" + +tsutils@^3.0.0: + version "3.17.1" + resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" + integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== + dependencies: + tslib "^1.8.1" + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"