diff --git a/README.md b/README.md index ea32e07..e3e4fe0 100644 --- a/README.md +++ b/README.md @@ -315,7 +315,7 @@ Abstract methods for CRUDs: ```js .then(() => { // prepare the resource handler - const res = JSONSchemaSequelizer.resource(builder, null, 'Tag'); + const res = JSONSchemaSequelizer.resource(builder, 'Tag'); // resource details, references and UI console.log(JSON.stringify(res.options, null, 2)); @@ -423,7 +423,7 @@ RESTful API in ~80 LOC: }; // resource handler and options - const obj = JSONSchemaSequelizer.resource(builder, null, model); + const obj = JSONSchemaSequelizer.resource(builder, model); // write operations if (req.method === 'POST') { diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..a920021 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,137 @@ +import type { + Model, ModelCtor, ModelOptions, Sequelize, Options, QueryInterface, + FindOptions, CountOptions, CreateOptions, UpdateOptions, DestroyOptions, SyncOptions, +} from 'sequelize'; +import type { JsonObject } from 'type-fest'; +import type { DownToOptions, UpToOptions, Migration } from 'umzug'; + +import type { JSONSchema4 } from 'json-schema'; +export type { JSONSchema4 } from 'json-schema'; + +export * from 'sequelize'; + +interface UmzugInterface { + run(script: string): Promise; + up(opts?: UpToOptions): Promise; + down(opts?: DownToOptions): Promise; + next(): Promise; + prev(): Promise; + status(): { + pending: string[], + executed: string[], + }; +} + +interface UmzugMigrateCallback { + (query: QueryInterface, sequelize: Sequelize, promise?: typeof Promise): Promise; +} + +type UmzugMigrateOptions = { + configFile?: string; + baseDir?: string; + logging?: boolean | ((sql: string, timing?: number) => void); + database?: { + modelName?: string; + tableName?: string; + columnName?: string; + }; +}; + +interface ConnectionSettings extends Options { + connection?: string; +} + +interface JSONSchemaSequelizerMap { + (def: ModelDefinition, name: string, sequelize: Sequelize): ModelDefinition; +} + +export type JSONSchemaSequelizerRefs = JSONSchema4[] | { [k: string]: JSONSchema4 }; +export type JSONSchemaSequelizerDefs = { definitions?: { [k: string]: JSONSchema4 } }; + +declare function JSONSchemaSequelizer(settings: ConnectionSettings, refs?: JSONSchemaSequelizerRefs, cwd?: string): ResourceRepository; +declare namespace JSONSchemaSequelizer { + var migrate: { + (sequelize: Sequelize, options: { [key: string]: UmzugMigrateCallback }, bind: true): { [key: string]: () => Promise }; + (sequelize: Sequelize, options: UmzugMigrateOptions, bind?: boolean): UmzugInterface; + } + var bundle: (schemas: JSONSchema4[], definitions?: string | { [k: string]: JSONSchema4 }, description?: string) => void; + var generate: (dump: JSONSchemaSequelizerDefs | null, models: Model[], squash?: boolean, globalOptions?: ModelOptions) => void; + var scan: (cwd: string, cb: JSONSchemaSequelizerMap) => ModelDefinition[]; + var refs: (cwd: string, prefix?: string) => JSONSchema4[]; + var sync: (deps: Model[], opts?: SyncOptions) => Promise; + var clear: (deps: Model[], opts?: DestroyOptions) => Promise; +} + +export interface JSONSchemaSequelizerInterface { + add(model: ModelDefinition, isClass: true): ModelCtor; + add(model: ModelDefinition, isClass?: boolean): this; + scan(cb: Function): this; + refs(cwd: string, prefix: string): this; + sync(opts: SyncOptions): Promise; + close(): this; + ready(cb: Function): this; + connect(): Promise; +} + +export interface ResourceRepositoryOf extends JSONSchemaSequelizerInterface { + resource(name: keyof DB, opts?: ResourceOptions): ResourceModel; +} + +export interface ResourceRepository extends JSONSchemaSequelizerInterface { + resource(name: string, opts?: ResourceOptions): ResourceModel; +} + +export interface ResourceAttachment { + path: string; + name?: string; + size?: number; + type?: string; + lastModifiedDate?: string; +} + +export interface ResourceOptions { + raw?: boolean; + keys?: string[]; + where?: string; + payload?: JsonObject; + logging?: boolean | ((sql: string, timing?: number) => void); + noupdate?: boolean; + fallthrough?: boolean; + attachments?: { + files?: { + [key: string]: ResourceAttachment | ResourceAttachment[]; + }; + baseDir?: string; + uploadDir?: string; + }; + upload?(params: { + payload: JsonObject, + metadata: ResourceAttachment, + destFile: string, + field: string, + schema: JSONSchema4, + }): Promise; +} + +export interface ResourceModel { + options: { + model: string; + refs: { [key: string]: JSONSchema4 }; + schema: JSONSchema4; + uiSchema: JsonObject; + attributes: JsonObject; + }; + actions: { + update(payload: JsonObject, opts?: UpdateOptions): Promise<[number, number | undefined]>; + create(payload: JsonObject, opts?: CreateOptions): Promise<[T, JsonObject]>; + findAll(opts?: FindOptions): Promise; + findOne(opts?: FindOptions): Promise; + destroy(opts?: DestroyOptions): Promise; + count(opts?: CountOptions): Promise; + }; +} + +export interface ModelDefinition { + $class?: ModelCtor; + $schema: JSONSchema4; +} diff --git a/lib/index.js b/lib/index.js index 5697df9..734109b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -341,8 +341,8 @@ function JSONSchemaSequelizer(settings, refs, cwd) { }; // RESTful helper - this.resource = (options, modelName) => { - return JSONSchemaSequelizer.resource(this, options, modelName); + this.resource = (name, options) => { + return JSONSchemaSequelizer.resource(this, options, name); }; } diff --git a/package-lock.json b/package-lock.json index 21c235c..dcc13d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -409,17 +409,82 @@ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true }, + "@types/bluebird": { + "version": "3.5.36", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.36.tgz", + "integrity": "sha512-HBNx4lhkxN7bx6P0++W8E289foSu8kO8GCk2unhuVggO+cE7rh9DhZUyPhUxNRG9m+5B5BTKxZQ5ZP92x/mx9Q==" + }, + "@types/bson": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.5.tgz", + "integrity": "sha512-vVLwMUqhYJSQ/WKcE60eFqcyuWse5fGH+NMAXHuKrUAPoryq3ATxk5o4bgYNtg5aOM4APVg7Hnb3ASqUYG0PKg==", + "requires": { + "@types/node": "*" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "@types/continuation-local-storage": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@types/continuation-local-storage/-/continuation-local-storage-3.2.3.tgz", + "integrity": "sha512-4LYeWblV+6puK9tFGM7Zr4OLZkVXmaL7hUK6/wHwbfwM+q7v+HZyBWTXkNOiC9GqOxv7ehhi5TMCbebZWeVYtw==", + "requires": { + "@types/node": "*" + } + }, + "@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==" + }, + "@types/lodash": { + "version": "4.14.172", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.172.tgz", + "integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==" + }, + "@types/mongodb": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.20.tgz", + "integrity": "sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==", + "requires": { + "@types/bson": "*", + "@types/node": "*" + } + }, "@types/node": { "version": "16.0.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.0.0.tgz", "integrity": "sha512-TmCW5HoZ2o2/z2EYi109jLqIaPIi9y/lc2LmDCWzuCi35bcaQ+OtUh6nwBiFK7SOu25FAU5+YKdqFZUwtqGSdg==" }, + "@types/sequelize": { + "version": "4.28.10", + "resolved": "https://registry.npmjs.org/@types/sequelize/-/sequelize-4.28.10.tgz", + "integrity": "sha512-GKbEbl6uyEYTPvU2JZvmqZHfpwTTjaZvNSd2gFJrhcxUL1bcyG7i+S8Od2L0/+skrk2bBINl7J1Sugo0mgIY3g==", + "requires": { + "@types/bluebird": "*", + "@types/continuation-local-storage": "*", + "@types/lodash": "*", + "@types/validator": "*" + } + }, + "@types/umzug": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/umzug/-/umzug-2.3.2.tgz", + "integrity": "sha512-ozfpVpUwzozBVVII2T1iUTByJ1n+xgi3xpjzUnZ50tnAhBCzQ+RiewGgjyzuK2RAb3cug0mqKTvMbvd336lkDw==", + "requires": { + "@types/mongodb": "^3.6.20", + "@types/sequelize": "*" + } + }, + "@types/validator": { + "version": "13.6.3", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.6.3.tgz", + "integrity": "sha512-fWG42pMJOL4jKsDDZZREnXLjc3UE0R8LOJfARWYg6U966rxDT7TYejYzLnUF5cvSObGg34nd0+H2wHHU5Omdfw==" + }, "@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", @@ -1297,6 +1362,14 @@ "dev": true, "requires": { "type-fest": "^0.8.1" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } } }, "has-flag": { @@ -2004,6 +2077,14 @@ "requires": { "is-stream": "^2.0.0", "type-fest": "^0.8.0" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } } }, "he": { @@ -4497,10 +4578,9 @@ "dev": true }, "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.0.0.tgz", + "integrity": "sha512-BoEUnckjP9oiudy3KxlGdudtBAdJQ74Wp7dYwVPkUzBn+cVHOsBXh2zD2jLyqgbuJ1KMNriczZCI7lTBA94dFg==" }, "typedarray-to-buffer": { "version": "3.1.5", diff --git a/package.json b/package.json index c27b7d3..00847da 100644 --- a/package.json +++ b/package.json @@ -41,10 +41,13 @@ "pg-native": "^3.0.0" }, "dependencies": { + "@types/json-schema": "^7.0.9", + "@types/umzug": "^2.3.2", "fs-extra": "^9.0.0", "glob": "^7.1.2", "json-schema-ref-parser": "^9.0.1", "sequelize": "^6.6.5", + "type-fest": "^2.0.0", "umzug": "^2.0.1", "wargs": "^0.9.1" } diff --git a/tests/res.test.js b/tests/res.test.js index 8b16b1e..e9a5269 100644 --- a/tests/res.test.js +++ b/tests/res.test.js @@ -87,9 +87,8 @@ settings.forEach(config => { price: 1.23, }); }).then(() => { - return Cart.actions.create(data) - .then(row => jss.models.Cart.findByPk(row.id)); - }).then(row => { + return Cart.actions.create(data); + }).then(([row]) => { return row.getItems({ order: ['createdAt'], }); @@ -107,7 +106,7 @@ settings.forEach(config => { const data = { items: [ { - qty: 2, + qty: 99, ProductId: 1, }, ], @@ -119,8 +118,11 @@ settings.forEach(config => { id: 1, }, }); - }).then(row => { - expect(row.id).to.eql(1); + }).then(([affected]) => { + expect(affected).to.eql(0); + return Cart.actions.findOne({ id: 1 }).then(x => { + expect(x.items[1].qty).to.eql(99); + }); }); }); @@ -129,7 +131,7 @@ settings.forEach(config => { where: { id: 1, items: { - qty: [2, 5], + qty: [99, 5], }, }, items: { @@ -160,7 +162,7 @@ settings.forEach(config => { }, { name: 'Test', price: 1.23, - quantity: 2, + quantity: 99, }, ], }); @@ -170,7 +172,8 @@ settings.forEach(config => { it('should destroy data from given associations', () => { return Promise.resolve().then(() => { return Cart.actions.destroy({ where: { id: 1 } }); - }).then(() => { + }).then(affected => { + expect(affected).to.eql(1); return Promise.all([jss.models.CartItem.count(), jss.models.Product.count(), jss.models.Cart.count()]); }).then(result => { expect(result).to.eql([0, 2, 0]); @@ -186,7 +189,7 @@ settings.forEach(config => { { qty: 2, Product: { id: 1, name: 'Test', price: 1.23 } }, ], })) - .then(row => Cart.actions.findOne({ where: { id: row.id } }).then(x => { + .then(([row]) => Cart.actions.findOne({ where: { id: row.id } }).then(x => { expect((x.items.reduce((a, b) => a + (b.Product.price * b.qty), 0)).toFixed(2)).to.eql('4.84'); })) .then(() => Cart.actions.count().then(x => expect(x).to.eql(1))) @@ -202,7 +205,7 @@ settings.forEach(config => { { id: 5, qty: 1, Product: { id: 2 } }, ], }, { where: { id: 2 } })) - .then(row => Cart.actions.findOne({ where: { id: row.id } }).then(x => { + .then(() => Cart.actions.findOne({ where: { id: 2 } }).then(x => { expect((x.items.reduce((a, b) => a + (b.Product.price * b.qty), 0)).toFixed(2)).to.eql('2.62'); })); }); @@ -258,7 +261,7 @@ settings.forEach(config => { { $upload: 'baz' }, ], })) - .then(result => { + .then(([, result]) => { expect(result.image.imageId).to.eql(result.id); expect(result.image2.image2Id).to.eql(result.id); expect(result.image2.path).to.eql('uploads/bar'); @@ -303,7 +306,7 @@ settings.forEach(config => { }, ], })) - .then(row => Cart.actions.findOne({ where: { id: row.id } })) + .then(([row]) => Cart.actions.findOne({ where: { id: row.id } })) .then(result => { expect(result.items[0].Product.image.path).to.eql('uploads/bar'); return Product.actions.findOne({ where: { id: result.items[0].ProductId } }) @@ -328,7 +331,7 @@ settings.forEach(config => { .then(() => Attachment.actions.create({ label: 'test', FileId: { $upload: 'ok' }, - })).then(result => { + })).then(([, result]) => { expect(result).to.eql({ label: 'test', FileId: 6, id: 1 }); }); });