|
|
@@ -0,0 +1,661 @@ |
|
|
|
import { |
|
|
|
beforeAll, |
|
|
|
afterAll, |
|
|
|
afterEach, |
|
|
|
beforeEach, |
|
|
|
describe, |
|
|
|
expect, |
|
|
|
it, |
|
|
|
} from 'vitest'; |
|
|
|
import { |
|
|
|
tmpdir |
|
|
|
} from 'os'; |
|
|
|
import { |
|
|
|
mkdtemp, |
|
|
|
rm, |
|
|
|
writeFile, |
|
|
|
} from 'fs/promises'; |
|
|
|
import { |
|
|
|
join |
|
|
|
} from 'path'; |
|
|
|
import { |
|
|
|
application, |
|
|
|
DataSource, |
|
|
|
dataSources, |
|
|
|
encodings, |
|
|
|
Resource, |
|
|
|
resource, |
|
|
|
serializers, |
|
|
|
valibot as v, |
|
|
|
} from '../../src'; |
|
|
|
import {request, Server} from 'http'; |
|
|
|
import {constants} from 'http2'; |
|
|
|
|
|
|
|
const PORT = 3000; |
|
|
|
const HOST = 'localhost'; |
|
|
|
const ACCEPT_ENCODING = 'utf-8'; |
|
|
|
const ACCEPT = 'application/json'; |
|
|
|
|
|
|
|
const autoIncrement = async (dataSource: DataSource) => { |
|
|
|
const data = await dataSource.getMultiple() as Record<string, string>[]; |
|
|
|
|
|
|
|
const highestId = data.reduce<number>( |
|
|
|
(highestId, d) => (Number(d.id) > highestId ? Number(d.id) : highestId), |
|
|
|
-Infinity |
|
|
|
); |
|
|
|
|
|
|
|
if (Number.isFinite(highestId)) { |
|
|
|
return (highestId + 1); |
|
|
|
} |
|
|
|
|
|
|
|
return 1; |
|
|
|
}; |
|
|
|
|
|
|
|
describe('yasumi', () => { |
|
|
|
let baseDir: string; |
|
|
|
beforeAll(async () => { |
|
|
|
try { |
|
|
|
baseDir = await mkdtemp(join(tmpdir(), 'yasumi-')); |
|
|
|
} catch { |
|
|
|
// noop |
|
|
|
} |
|
|
|
}); |
|
|
|
afterAll(async () => { |
|
|
|
try { |
|
|
|
await rm(baseDir, { |
|
|
|
recursive: true, |
|
|
|
}); |
|
|
|
} catch { |
|
|
|
// noop |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
let Piano: Resource; |
|
|
|
beforeEach(() => { |
|
|
|
Piano = resource(v.object( |
|
|
|
{ |
|
|
|
brand: v.string() |
|
|
|
}, |
|
|
|
v.never() |
|
|
|
)) |
|
|
|
.name('Piano') |
|
|
|
.id('id', { |
|
|
|
generationStrategy: autoIncrement, |
|
|
|
serialize: (id) => id?.toString() ?? '0', |
|
|
|
deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0, |
|
|
|
}) |
|
|
|
}); |
|
|
|
|
|
|
|
let server: Server; |
|
|
|
beforeEach(() => { |
|
|
|
const app = application({ |
|
|
|
name: 'piano-service', |
|
|
|
dataSource: (resource) => new dataSources.jsonlFile.DataSource(resource, baseDir), |
|
|
|
}) |
|
|
|
.contentType(ACCEPT, serializers.applicationJson) |
|
|
|
.encoding(ACCEPT_ENCODING, encodings.utf8) |
|
|
|
.resource(Piano); |
|
|
|
|
|
|
|
server = app.createServer({ |
|
|
|
baseUrl: '/api' |
|
|
|
}); |
|
|
|
|
|
|
|
return new Promise((resolve, reject) => { |
|
|
|
server.on('error', (err) => { |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
server.on('listening', () => { |
|
|
|
resolve(); |
|
|
|
}); |
|
|
|
|
|
|
|
server.listen({ |
|
|
|
port: PORT |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
afterEach(() => new Promise((resolve, reject) => { |
|
|
|
server.close((err) => { |
|
|
|
if (err) { |
|
|
|
reject(err); |
|
|
|
} |
|
|
|
|
|
|
|
resolve(); |
|
|
|
}); |
|
|
|
})); |
|
|
|
|
|
|
|
describe('serving collections', () => { |
|
|
|
it('returns data', () => { |
|
|
|
return new Promise<void>((resolve, reject) => { |
|
|
|
Piano.allowFetchCollection(); |
|
|
|
|
|
|
|
const req = request( |
|
|
|
{ |
|
|
|
host: HOST, |
|
|
|
port: PORT, |
|
|
|
path: '/api/pianos', |
|
|
|
method: 'GET', |
|
|
|
headers: { |
|
|
|
'Accept': ACCEPT, |
|
|
|
'Accept-Encoding': ACCEPT_ENCODING, |
|
|
|
}, |
|
|
|
}, |
|
|
|
(res) => { |
|
|
|
res.on('error', (err) => { |
|
|
|
Piano.revokeFetchCollection(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK); |
|
|
|
expect(res.headers).toHaveProperty('content-type', ACCEPT); |
|
|
|
|
|
|
|
let resBuffer = Buffer.from(''); |
|
|
|
res.on('data', (c) => { |
|
|
|
resBuffer = Buffer.concat([resBuffer, c]); |
|
|
|
}); |
|
|
|
|
|
|
|
res.on('close', () => { |
|
|
|
const resBufferJson = resBuffer.toString(ACCEPT_ENCODING); |
|
|
|
const resData = JSON.parse(resBufferJson); |
|
|
|
expect(resData).toEqual([]); |
|
|
|
Piano.revokeFetchCollection(); |
|
|
|
resolve(); |
|
|
|
}); |
|
|
|
}, |
|
|
|
); |
|
|
|
|
|
|
|
req.on('error', (err) => { |
|
|
|
Piano.revokeFetchCollection(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
req.end(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
describe('serving items', () => { |
|
|
|
const data = { |
|
|
|
id: 1, |
|
|
|
brand: 'Yamaha' |
|
|
|
}; |
|
|
|
|
|
|
|
beforeEach(async () => { |
|
|
|
const resourcePath = join(baseDir, 'pianos.jsonl'); |
|
|
|
await writeFile(resourcePath, JSON.stringify(data)); |
|
|
|
}); |
|
|
|
|
|
|
|
it('returns data', () => { |
|
|
|
return new Promise<void>((resolve, reject) => { |
|
|
|
Piano.allowFetchItem(); |
|
|
|
|
|
|
|
const req = request( |
|
|
|
{ |
|
|
|
host: HOST, |
|
|
|
port: PORT, |
|
|
|
path: '/api/pianos/1', |
|
|
|
method: 'GET', |
|
|
|
headers: { |
|
|
|
'Accept': ACCEPT, |
|
|
|
'Accept-Encoding': ACCEPT_ENCODING, |
|
|
|
}, |
|
|
|
}, |
|
|
|
(res) => { |
|
|
|
res.on('error', (err) => { |
|
|
|
Piano.revokeFetchItem(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK); |
|
|
|
expect(res.headers).toHaveProperty('content-type', ACCEPT); |
|
|
|
|
|
|
|
let resBuffer = Buffer.from(''); |
|
|
|
res.on('data', (c) => { |
|
|
|
resBuffer = Buffer.concat([resBuffer, c]); |
|
|
|
}); |
|
|
|
|
|
|
|
res.on('close', () => { |
|
|
|
const resBufferJson = resBuffer.toString(ACCEPT_ENCODING); |
|
|
|
const resData = JSON.parse(resBufferJson); |
|
|
|
expect(resData).toEqual(data); |
|
|
|
Piano.revokeFetchItem(); |
|
|
|
resolve(); |
|
|
|
}); |
|
|
|
}, |
|
|
|
); |
|
|
|
|
|
|
|
req.on('error', (err) => { |
|
|
|
Piano.revokeFetchItem(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
req.end(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
it('throws on item not found', () => { |
|
|
|
return new Promise<void>((resolve, reject) => { |
|
|
|
Piano.allowFetchItem(); |
|
|
|
|
|
|
|
const req = request( |
|
|
|
{ |
|
|
|
host: HOST, |
|
|
|
port: PORT, |
|
|
|
path: '/api/pianos/2', |
|
|
|
method: 'GET', |
|
|
|
headers: { |
|
|
|
'Accept': ACCEPT, |
|
|
|
'Accept-Encoding': ACCEPT_ENCODING, |
|
|
|
}, |
|
|
|
}, |
|
|
|
(res) => { |
|
|
|
res.on('error', (err) => { |
|
|
|
Piano.revokeFetchItem(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NOT_FOUND); |
|
|
|
|
|
|
|
Piano.revokeFetchItem(); |
|
|
|
resolve(); |
|
|
|
}, |
|
|
|
); |
|
|
|
|
|
|
|
req.on('error', (err) => { |
|
|
|
Piano.revokeFetchItem(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
req.end(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
describe('creating items', () => { |
|
|
|
const data = { |
|
|
|
id: 1, |
|
|
|
brand: 'Yamaha' |
|
|
|
}; |
|
|
|
|
|
|
|
const newData = { |
|
|
|
brand: 'K. Kawai' |
|
|
|
}; |
|
|
|
|
|
|
|
beforeEach(async () => { |
|
|
|
const resourcePath = join(baseDir, 'pianos.jsonl'); |
|
|
|
await writeFile(resourcePath, JSON.stringify(data)); |
|
|
|
}); |
|
|
|
|
|
|
|
// FIXME ID de/serialization problems |
|
|
|
it('returns data', () => { |
|
|
|
return new Promise<void>((resolve, reject) => { |
|
|
|
Piano.allowCreate(); |
|
|
|
|
|
|
|
const req = request( |
|
|
|
{ |
|
|
|
host: HOST, |
|
|
|
port: PORT, |
|
|
|
path: '/api/pianos', |
|
|
|
method: 'POST', |
|
|
|
headers: { |
|
|
|
'Accept': ACCEPT, |
|
|
|
'Accept-Encoding': ACCEPT_ENCODING, |
|
|
|
'Content-Type': ACCEPT, |
|
|
|
}, |
|
|
|
}, |
|
|
|
(res) => { |
|
|
|
res.on('error', (err) => { |
|
|
|
Piano.revokeCreate(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_CREATED); |
|
|
|
expect(res.headers).toHaveProperty('content-type', ACCEPT); |
|
|
|
|
|
|
|
let resBuffer = Buffer.from(''); |
|
|
|
res.on('data', (c) => { |
|
|
|
resBuffer = Buffer.concat([resBuffer, c]); |
|
|
|
}); |
|
|
|
|
|
|
|
res.on('close', () => { |
|
|
|
const resBufferJson = resBuffer.toString(ACCEPT_ENCODING); |
|
|
|
const resData = JSON.parse(resBufferJson); |
|
|
|
expect(resData).toEqual({ |
|
|
|
...newData, |
|
|
|
id: '2' |
|
|
|
}); |
|
|
|
Piano.revokeCreate(); |
|
|
|
resolve(); |
|
|
|
}); |
|
|
|
}, |
|
|
|
); |
|
|
|
|
|
|
|
req.on('error', (err) => { |
|
|
|
Piano.revokeCreate(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
req.write(JSON.stringify(newData)); |
|
|
|
req.end(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
describe('patching items', () => { |
|
|
|
const data = { |
|
|
|
id: 1, |
|
|
|
brand: 'Yamaha' |
|
|
|
}; |
|
|
|
|
|
|
|
const newData = { |
|
|
|
brand: 'K. Kawai' |
|
|
|
}; |
|
|
|
|
|
|
|
beforeEach(async () => { |
|
|
|
const resourcePath = join(baseDir, 'pianos.jsonl'); |
|
|
|
await writeFile(resourcePath, JSON.stringify(data)); |
|
|
|
}); |
|
|
|
|
|
|
|
it('returns data', () => { |
|
|
|
return new Promise<void>((resolve, reject) => { |
|
|
|
Piano.allowPatch(); |
|
|
|
|
|
|
|
const req = request( |
|
|
|
{ |
|
|
|
host: HOST, |
|
|
|
port: PORT, |
|
|
|
path: '/api/pianos/1', |
|
|
|
method: 'PATCH', |
|
|
|
headers: { |
|
|
|
'Accept': ACCEPT, |
|
|
|
'Accept-Encoding': ACCEPT_ENCODING, |
|
|
|
'Content-Type': ACCEPT, |
|
|
|
}, |
|
|
|
}, |
|
|
|
(res) => { |
|
|
|
res.on('error', (err) => { |
|
|
|
Piano.revokePatch(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK); |
|
|
|
expect(res.headers).toHaveProperty('content-type', ACCEPT); |
|
|
|
|
|
|
|
let resBuffer = Buffer.from(''); |
|
|
|
res.on('data', (c) => { |
|
|
|
resBuffer = Buffer.concat([resBuffer, c]); |
|
|
|
}); |
|
|
|
|
|
|
|
res.on('close', () => { |
|
|
|
const resBufferJson = resBuffer.toString(ACCEPT_ENCODING); |
|
|
|
const resData = JSON.parse(resBufferJson); |
|
|
|
expect(resData).toEqual({ |
|
|
|
...data, |
|
|
|
...newData, |
|
|
|
}); |
|
|
|
Piano.revokePatch(); |
|
|
|
resolve(); |
|
|
|
}); |
|
|
|
}, |
|
|
|
); |
|
|
|
|
|
|
|
req.on('error', (err) => { |
|
|
|
Piano.revokePatch(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
req.write(JSON.stringify(newData)); |
|
|
|
req.end(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
it('throws on item to patch not found', () => { |
|
|
|
return new Promise<void>((resolve, reject) => { |
|
|
|
Piano.allowPatch(); |
|
|
|
|
|
|
|
const req = request( |
|
|
|
{ |
|
|
|
host: HOST, |
|
|
|
port: PORT, |
|
|
|
path: '/api/pianos/2', |
|
|
|
method: 'PATCH', |
|
|
|
headers: { |
|
|
|
'Accept': ACCEPT, |
|
|
|
'Accept-Encoding': ACCEPT_ENCODING, |
|
|
|
'Content-Type': ACCEPT, |
|
|
|
}, |
|
|
|
}, |
|
|
|
(res) => { |
|
|
|
res.on('error', (err) => { |
|
|
|
Piano.revokePatch(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NOT_FOUND); |
|
|
|
|
|
|
|
Piano.revokePatch(); |
|
|
|
resolve(); |
|
|
|
}, |
|
|
|
); |
|
|
|
|
|
|
|
req.on('error', (err) => { |
|
|
|
Piano.revokePatch(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
req.write(JSON.stringify(newData)); |
|
|
|
req.end(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
describe('emplacing items', () => { |
|
|
|
const data = { |
|
|
|
id: 1, |
|
|
|
brand: 'Yamaha' |
|
|
|
}; |
|
|
|
|
|
|
|
const newData = { |
|
|
|
id: 1, |
|
|
|
brand: 'K. Kawai' |
|
|
|
}; |
|
|
|
|
|
|
|
beforeEach(async () => { |
|
|
|
const resourcePath = join(baseDir, 'pianos.jsonl'); |
|
|
|
await writeFile(resourcePath, JSON.stringify(data)); |
|
|
|
}); |
|
|
|
|
|
|
|
// FIXME IDs not properly being de/serialized |
|
|
|
it('returns data for replacement', () => { |
|
|
|
return new Promise<void>((resolve, reject) => { |
|
|
|
Piano.allowEmplace(); |
|
|
|
|
|
|
|
const req = request( |
|
|
|
{ |
|
|
|
host: HOST, |
|
|
|
port: PORT, |
|
|
|
path: '/api/pianos/1', |
|
|
|
method: 'PUT', |
|
|
|
headers: { |
|
|
|
'Accept': ACCEPT, |
|
|
|
'Accept-Encoding': ACCEPT_ENCODING, |
|
|
|
'Content-Type': ACCEPT, |
|
|
|
}, |
|
|
|
}, |
|
|
|
(res) => { |
|
|
|
res.on('error', (err) => { |
|
|
|
Piano.revokeEmplace(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK); |
|
|
|
expect(res.headers).toHaveProperty('content-type', ACCEPT); |
|
|
|
|
|
|
|
let resBuffer = Buffer.from(''); |
|
|
|
res.on('data', (c) => { |
|
|
|
resBuffer = Buffer.concat([resBuffer, c]); |
|
|
|
}); |
|
|
|
|
|
|
|
res.on('close', () => { |
|
|
|
const resBufferJson = resBuffer.toString(ACCEPT_ENCODING); |
|
|
|
const resData = JSON.parse(resBufferJson); |
|
|
|
expect(resData).toEqual(newData); |
|
|
|
Piano.revokeEmplace(); |
|
|
|
resolve(); |
|
|
|
}); |
|
|
|
}, |
|
|
|
); |
|
|
|
|
|
|
|
req.on('error', (err) => { |
|
|
|
Piano.revokeEmplace(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
req.write(JSON.stringify(newData)); |
|
|
|
req.end(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
it('returns data for creation', () => { |
|
|
|
return new Promise<void>((resolve, reject) => { |
|
|
|
const id = 2; |
|
|
|
Piano.allowEmplace(); |
|
|
|
|
|
|
|
const req = request( |
|
|
|
{ |
|
|
|
host: HOST, |
|
|
|
port: PORT, |
|
|
|
path: `/api/pianos/${id}`, |
|
|
|
method: 'PUT', |
|
|
|
headers: { |
|
|
|
'Accept': ACCEPT, |
|
|
|
'Accept-Encoding': ACCEPT_ENCODING, |
|
|
|
'Content-Type': ACCEPT, |
|
|
|
}, |
|
|
|
}, |
|
|
|
(res) => { |
|
|
|
res.on('error', (err) => { |
|
|
|
Piano.revokeEmplace(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_CREATED); |
|
|
|
expect(res.headers).toHaveProperty('content-type', ACCEPT); |
|
|
|
|
|
|
|
let resBuffer = Buffer.from(''); |
|
|
|
res.on('data', (c) => { |
|
|
|
resBuffer = Buffer.concat([resBuffer, c]); |
|
|
|
}); |
|
|
|
|
|
|
|
res.on('close', () => { |
|
|
|
const resBufferJson = resBuffer.toString(ACCEPT_ENCODING); |
|
|
|
const resData = JSON.parse(resBufferJson); |
|
|
|
expect(resData).toEqual({ |
|
|
|
...newData, |
|
|
|
id, |
|
|
|
}); |
|
|
|
Piano.revokeEmplace(); |
|
|
|
resolve(); |
|
|
|
}); |
|
|
|
}, |
|
|
|
); |
|
|
|
|
|
|
|
req.on('error', (err) => { |
|
|
|
Piano.revokeEmplace(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
req.write(JSON.stringify({ |
|
|
|
...newData, |
|
|
|
id, |
|
|
|
})); |
|
|
|
req.end(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
describe('deleting items', () => { |
|
|
|
const data = { |
|
|
|
id: 1, |
|
|
|
brand: 'Yamaha' |
|
|
|
}; |
|
|
|
|
|
|
|
beforeEach(async () => { |
|
|
|
const resourcePath = join(baseDir, 'pianos.jsonl'); |
|
|
|
await writeFile(resourcePath, JSON.stringify(data)); |
|
|
|
}); |
|
|
|
|
|
|
|
it('returns data', () => { |
|
|
|
return new Promise<void>((resolve, reject) => { |
|
|
|
Piano.allowDelete(); |
|
|
|
|
|
|
|
const req = request( |
|
|
|
{ |
|
|
|
host: HOST, |
|
|
|
port: PORT, |
|
|
|
path: '/api/pianos/1', |
|
|
|
method: 'DELETE', |
|
|
|
headers: { |
|
|
|
'Accept': ACCEPT, |
|
|
|
'Accept-Encoding': ACCEPT_ENCODING, |
|
|
|
}, |
|
|
|
}, |
|
|
|
(res) => { |
|
|
|
res.on('error', (err) => { |
|
|
|
Piano.revokeDelete(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT); |
|
|
|
Piano.revokeDelete(); |
|
|
|
resolve(); |
|
|
|
}, |
|
|
|
); |
|
|
|
|
|
|
|
req.on('error', (err) => { |
|
|
|
Piano.revokeDelete(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
req.end(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
it('throws on item not found', () => { |
|
|
|
return new Promise<void>((resolve, reject) => { |
|
|
|
Piano.allowDelete(); |
|
|
|
|
|
|
|
const req = request( |
|
|
|
{ |
|
|
|
host: HOST, |
|
|
|
port: PORT, |
|
|
|
path: '/api/pianos/2', |
|
|
|
method: 'DELETE', |
|
|
|
headers: { |
|
|
|
'Accept': ACCEPT, |
|
|
|
'Accept-Encoding': ACCEPT_ENCODING, |
|
|
|
}, |
|
|
|
}, |
|
|
|
(res) => { |
|
|
|
res.on('error', (err) => { |
|
|
|
Piano.revokeDelete(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NOT_FOUND); |
|
|
|
Piano.revokeDelete(); |
|
|
|
resolve(); |
|
|
|
}, |
|
|
|
); |
|
|
|
|
|
|
|
req.on('error', (err) => { |
|
|
|
Piano.revokeDelete(); |
|
|
|
reject(err); |
|
|
|
}); |
|
|
|
|
|
|
|
req.end(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |