UNPKG

apialize

Version:

Turn a database model into a production ready REST(ish) CRUD API in a few lines.

222 lines (191 loc) 7.35 kB
const express = require('express'); const bodyParser = require('body-parser'); const request = require('supertest'); const { Sequelize, DataTypes } = require('sequelize'); const { create, single, destroy } = require('../src'); async function build({ destroyOptions = {}, modelOptions = {} } = {}) { const sequelize = new Sequelize('sqlite::memory:', { logging: false }); const Item = sequelize.define( 'Item', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, external_id: { type: DataTypes.STRING(64), allowNull: false, unique: true, }, name: { type: DataTypes.STRING(100), allowNull: false }, user_id: { type: DataTypes.INTEGER, allowNull: true }, parent_id: { type: DataTypes.INTEGER, allowNull: true }, }, { tableName: 'destroy_items', timestamps: false } ); await sequelize.sync({ force: true }); const app = express(); app.use(bodyParser.json()); app.use('/items', create(Item)); app.use('/items', single(Item)); app.use('/items', destroy(Item, destroyOptions, modelOptions)); return { sequelize, Item, app }; } describe('destroy operation: comprehensive options coverage', () => { let sequelize; afterEach(async () => { if (sequelize) { await sequelize.close(); sequelize = null; } }); test('default id mapping deletes by numeric id and returns success with id', async () => { const { sequelize: s, app, Item } = await build(); sequelize = s; const created = await request(app) .post('/items') .send({ external_id: 'd1', name: 'A' }); const id = created.body.id; const del = await request(app).delete(`/items/${id}`); expect(del.status).toBe(200); expect(del.body).toMatchObject({ success: true, id: String(id) }); const get404 = await request(app).get(`/items/${id}`); expect(get404.status).toBe(404); }); test('id_mapping: external_id deletes by external id', async () => { const { sequelize: s, app } = await build({ destroyOptions: { id_mapping: 'external_id' }, }); sequelize = s; await request(app).post('/items').send({ external_id: 'ex-d1', name: 'A' }); const del = await request(app).delete(`/items/ex-d1`); expect(del.status).toBe(200); expect(del.body).toMatchObject({ success: true, id: 'ex-d1' }); const get404 = await request(app).get(`/items/ex-d1`); expect(get404.status).toBe(404); }); test('ownership scoping via query filters prevents deleting foreign records', async () => { const { sequelize: s, app } = await build(); sequelize = s; const a = await request(app) .post('/items') .send({ external_id: 'a', name: 'A', user_id: 1 }); const b = await request(app) .post('/items') .send({ external_id: 'b', name: 'B', user_id: 2 }); // Wrong scope -> 404 const miss = await request(app).delete(`/items/${b.body.id}?user_id=1`); expect(miss.status).toBe(404); // Right scope const ok = await request(app).delete(`/items/${a.body.id}?user_id=1`); expect(ok.status).toBe(200); }); test('middleware can enforce parent scoping via req.apialize.options.where', async () => { const scope = (req, _res, next) => { req.apialize.applyWhere({ parent_id: 50 }); next(); }; const { sequelize: s, app } = await build({ destroyOptions: { middleware: [scope] }, }); sequelize = s; const t1 = await request(app) .post('/items') .send({ external_id: 't1', name: 'T1', parent_id: 50 }); await request(app) .post('/items') .send({ external_id: 't2', name: 'T2', parent_id: 999 }); const ok = await request(app).delete(`/items/${t1.body.id}`); expect(ok.status).toBe(200); const miss = await request(app).delete(`/items/${t1.body.id}`); expect(miss.status).toBe(404); // already deleted under parent scope }); test('404 when record not found (default and custom mapping)', async () => { const { sequelize: s, app } = await build(); sequelize = s; const missDefault = await request(app).delete(`/items/9999`); expect(missDefault.status).toBe(404); const { sequelize: s2, app: app2 } = await build({ destroyOptions: { id_mapping: 'external_id' }, }); await request(app2) .post('/items') .send({ external_id: 'exists', name: 'X' }); const del = await request(app2).delete(`/items/exists`); expect(del.status).toBe(200); const missCustom = await request(app2).delete(`/items/not-exists`); expect(missCustom.status).toBe(404); await s2.close(); }); test('pre/post hooks: transaction present and payload mutated (destroy)', async () => { const { sequelize: s, app } = await build({ destroyOptions: { pre: async (context) => { expect(context.transaction).toBeTruthy(); expect(typeof context.transaction.commit).toBe('function'); return { ran: true }; }, post: async (context) => { expect(context.preResult).toEqual({ ran: true }); context.payload.extra = 'ok'; }, }, }); sequelize = s; const created = await request(app) .post('/items') .send({ external_id: 'd-hook', name: 'X' }); const id = created.body.id; const del = await request(app).delete(`/items/${id}`); expect(del.status).toBe(200); expect(del.body.success).toBe(true); expect(del.body.extra).toBe('ok'); }); test('array pre/post hooks: multiple functions execute in order (destroy)', async () => { const executionOrder = []; const { sequelize: s, app } = await build({ destroyOptions: { pre: [ async (context) => { executionOrder.push('pre1'); expect(context.transaction).toBeTruthy(); return { step: 1 }; }, async (context) => { executionOrder.push('pre2'); expect(context.transaction).toBeTruthy(); return { step: 2 }; }, async (context) => { executionOrder.push('pre3'); expect(context.transaction).toBeTruthy(); return { step: 3, finalPre: true }; }, ], post: [ async (context) => { executionOrder.push('post1'); expect(context.preResult).toEqual({ step: 3, finalPre: true }); context.payload.hook1 = 'executed'; }, async (context) => { executionOrder.push('post2'); expect(context.payload.hook1).toBe('executed'); context.payload.hook2 = 'also-executed'; }, ], }, }); sequelize = s; // Create an item first const created = await request(app) .post('/items') .send({ external_id: 'array-hooks-d1', name: 'ArrayDestroyTest' }); expect(created.status).toBe(201); const id = created.body.id; // Then delete it with array hooks const deleted = await request(app).delete(`/items/${id}`); expect(deleted.status).toBe(200); expect(deleted.body.success).toBe(true); expect(deleted.body.hook1).toBe('executed'); expect(deleted.body.hook2).toBe('also-executed'); expect(executionOrder).toEqual(['pre1', 'pre2', 'pre3', 'post1', 'post2']); }); });