UNPKG

apialize

Version:

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

193 lines (169 loc) 6.19 kB
const express = require('express'); const bodyParser = require('body-parser'); const request = require('supertest'); const { Sequelize, DataTypes } = require('sequelize'); const { single, create } = require('../src'); // This suite verifies DELETE on a related collection without :id // - Dry run: confirm missing or false returns ids only // - Confirmed: confirm=true performs deletion and returns deleted count and ids // - id_mapping override is respected for returned ids describe('bulk delete on related collections', () => { let sequelize; let User; let Post; let Comment; let app; beforeAll(async () => { sequelize = new Sequelize('sqlite::memory:', { logging: false }); User = sequelize.define( 'User', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, name: { type: DataTypes.STRING(100), allowNull: false }, }, { tableName: 'bd_users', timestamps: false } ); Post = sequelize.define( 'Post', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, title: { type: DataTypes.STRING(200), allowNull: false }, user_id: { type: DataTypes.INTEGER, allowNull: false }, }, { tableName: 'bd_posts', timestamps: false } ); Comment = sequelize.define( 'Comment', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, comment_key: { type: DataTypes.STRING(64), allowNull: false, unique: true, }, text: { type: DataTypes.STRING(255), allowNull: false }, post_id: { type: DataTypes.INTEGER, allowNull: false }, }, { tableName: 'bd_comments', timestamps: false } ); await sequelize.sync({ force: true }); }); beforeEach(async () => { await Comment.destroy({ where: {} }); await Post.destroy({ where: {} }); await User.destroy({ where: {} }); app = express(); app.use(bodyParser.json()); // Mount endpoints with comments related under posts app.use('/users', create(User)); app.use( '/users', single(User, { related: [ { model: Post, operations: ['list', 'post', 'get'], related: [ { model: Comment, operations: ['list', 'post', 'get', 'delete'], // Per-op override to use custom id mapping for DELETE operations perOperation: { delete: { id_mapping: 'comment_key', allow_bulk_delete: true, }, }, }, ], }, ], }) ); }); afterAll(async () => { await sequelize.close(); }); test('dry run returns ids (no confirm)', async () => { const u = await request(app).post('/users').send({ name: 'U' }); const userId = u.body.id; const p = await request(app) .post(`/users/${userId}/posts`) .send({ title: 'T' }); const postId = p.body.id; // create 3 comments const c1 = await request(app) .post(`/users/${userId}/posts/${postId}/comments`) .send({ text: 'A', comment_key: 'k1' }); const c2 = await request(app) .post(`/users/${userId}/posts/${postId}/comments`) .send({ text: 'B', comment_key: 'k2' }); const c3 = await request(app) .post(`/users/${userId}/posts/${postId}/comments`) .send({ text: 'C', comment_key: 'k3' }); expect(c1.status).toBe(201); expect(c2.status).toBe(201); expect(c3.status).toBe(201); const dry = await request(app).delete( `/users/${userId}/posts/${postId}/comments` ); expect(dry.status).toBe(200); expect(dry.body).toMatchObject({ success: true, confirm_required: true }); // ids reflect id_mapping override (comment_key) as strings expect(dry.body.ids.sort()).toEqual(['k1', 'k2', 'k3']); // Ensure records still exist const list = await request(app).get( `/users/${userId}/posts/${postId}/comments` ); expect(list.body.data).toHaveLength(3); }); test('confirmed bulk delete removes records and returns ids + count', async () => { const u = await request(app).post('/users').send({ name: 'U2' }); const userId = u.body.id; const p = await request(app) .post(`/users/${userId}/posts`) .send({ title: 'T2' }); const postId = p.body.id; await request(app) .post(`/users/${userId}/posts/${postId}/comments`) .send({ text: 'A', comment_key: 'd1' }); await request(app) .post(`/users/${userId}/posts/${postId}/comments`) .send({ text: 'B', comment_key: 'd2' }); const ok = await request(app).delete( `/users/${userId}/posts/${postId}/comments?confirm=true` ); expect(ok.status).toBe(200); expect(ok.body.success).toBe(true); expect(ok.body.ids.sort()).toEqual(['d1', 'd2']); expect(ok.body.deleted).toBe(2); const list = await request(app).get( `/users/${userId}/posts/${postId}/comments` ); expect(list.body.data).toHaveLength(0); }); test("scoping: other parent's records are untouched", async () => { const u1 = await request(app).post('/users').send({ name: 'A' }); const u2 = await request(app).post('/users').send({ name: 'B' }); const p1 = await request(app) .post(`/users/${u1.body.id}/posts`) .send({ title: 'X' }); const p2 = await request(app) .post(`/users/${u2.body.id}/posts`) .send({ title: 'Y' }); await request(app) .post(`/users/${u1.body.id}/posts/${p1.body.id}/comments`) .send({ text: '1', comment_key: 'x1' }); await request(app) .post(`/users/${u2.body.id}/posts/${p2.body.id}/comments`) .send({ text: '2', comment_key: 'y1' }); const ok = await request(app).delete( `/users/${u1.body.id}/posts/${p1.body.id}/comments?confirm=true` ); expect(ok.status).toBe(200); const remains = await request(app).get( `/users/${u2.body.id}/posts/${p2.body.id}/comments` ); expect(remains.body.data.map((r) => r.comment_key)).toEqual(['y1']); }); });