DocApi: Implement DELETE on columns (#601) (#640)

* Factorize generateDocAndUrl
* Add describe for regrouping /records
This commit is contained in:
Florent 2023-08-24 14:33:53 +02:00 committed by GitHub
parent 9dfebefc9b
commit ee31764b83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 157 additions and 99 deletions

View File

@ -200,17 +200,20 @@ export async function handleSandboxErrorOnPlatform<T>(
platform.throwError('', `Invalid row id ${match[1]}`, 400); platform.throwError('', `Invalid row id ${match[1]}`, 400);
} }
match = message.match( match = message.match(
/\[Sandbox] (?:KeyError u?'(?:Table \w+ has no column )?|ValueError No such table: )(\w+)/ // eslint-disable-next-line max-len
/\[Sandbox] (?:KeyError u?'(?:Table \w+ has no column )?|ValueError No such table: |ValueError No such column: )([\w.]+)/
); );
if (match) { if (match) {
if (match[1] === tableId) { if (match[1] === tableId) {
platform.throwError('', `Table not found "${tableId}"`, 404); platform.throwError('', `Table not found "${tableId}"`, 404);
} else if (colNames.includes(match[1])) { } else if (colNames.includes(match[1])) {
platform.throwError('', `Invalid column "${match[1]}"`, 400); platform.throwError('', `Invalid column "${match[1]}"`, 400);
} else if (colNames.includes(match[1].replace(`${tableId}.`, ''))) {
platform.throwError('', `Table or column not found "${match[1]}"`, 404);
} }
} }
platform.throwError('', `Error manipulating data: ${message}`, 400); platform.throwError('', `Error manipulating data: ${message}`, 400);
} }
throw err; throw err;
} }
} }

View File

@ -746,6 +746,17 @@ export class DocWorkerApi {
}) })
); );
this._app.delete('/api/docs/:docId/tables/:tableId/columns/:colId', canEdit,
withDoc(async (activeDoc, req, res) => {
const {tableId, colId} = req.params;
const actions = [ [ 'RemoveColumn', tableId, colId ] ];
await handleSandboxError(tableId, [colId],
activeDoc.applyUserActions(docSessionFromRequest(req), actions)
);
res.json(null);
})
);
// Add a new webhook and trigger // Add a new webhook and trigger
this._app.post('/api/docs/:docId/webhooks', isOwner, validate(WebhookSubscribeCollection), this._app.post('/api/docs/:docId/webhooks', isOwner, validate(WebhookSubscribeCollection),
withDoc(async (activeDoc, req, res) => { withDoc(async (activeDoc, req, res) => {

View File

@ -877,124 +877,168 @@ function testDocApi() {
assert.equal(resp.status, 200); assert.equal(resp.status, 200);
}); });
describe("PUT /docs/{did}/columns", function () { describe("/docs/{did}/tables/{tid}/columns", function () {
async function generateDocAndUrl(docName: string = "Dummy") {
async function generateDocAndUrl() {
const wid = (await userApi.getOrgWorkspaces('current')).find((w) => w.name === 'Private')!.id; const wid = (await userApi.getOrgWorkspaces('current')).find((w) => w.name === 'Private')!.id;
const docId = await userApi.newDoc({name: 'ColumnsPut'}, wid); const docId = await userApi.newDoc({name: docName}, wid);
const url = `${serverUrl}/api/docs/${docId}/tables/Table1/columns`; const url = `${serverUrl}/api/docs/${docId}/tables/Table1/columns`;
return { url, docId }; return { url, docId };
} }
async function getColumnFieldsMapById(url: string, params: any) { describe("PUT /docs/{did}/tables/{tid}/columns", function () {
const result = await axios.get(url, {...chimpy, params}); async function getColumnFieldsMapById(url: string, params: any) {
assert.equal(result.status, 200); const result = await axios.get(url, {...chimpy, params});
return new Map<string, object>( assert.equal(result.status, 200);
result.data.columns.map( return new Map<string, object>(
({id, fields}: {id: string, fields: object}) => [id, fields] result.data.columns.map(
) ({id, fields}: {id: string, fields: object}) => [id, fields]
); )
} );
async function checkPut(
columns: [RecordWithStringId, ...RecordWithStringId[]],
params: Record<string, any>,
expectedFieldsByColId: Record<string, object>,
opts?: { getParams?: any }
) {
const {url} = await generateDocAndUrl();
const body: ColumnsPut = { columns };
const resp = await axios.put(url, body, {...chimpy, params});
assert.equal(resp.status, 200);
const fieldsByColId = await getColumnFieldsMapById(url, opts?.getParams);
assert.deepEqual(
[...fieldsByColId.keys()],
Object.keys(expectedFieldsByColId),
"The updated table should have the expected columns"
);
for (const [colId, expectedFields] of Object.entries(expectedFieldsByColId)) {
assert.deepInclude(fieldsByColId.get(colId), expectedFields);
} }
}
const COLUMN_TO_ADD = { async function checkPut(
id: "Foo", columns: [RecordWithStringId, ...RecordWithStringId[]],
fields: { params: Record<string, any>,
type: "Text", expectedFieldsByColId: Record<string, object>,
label: "FooLabel", opts?: { getParams?: any }
) {
const {url} = await generateDocAndUrl('ColumnsPut');
const body: ColumnsPut = { columns };
const resp = await axios.put(url, body, {...chimpy, params});
assert.equal(resp.status, 200);
const fieldsByColId = await getColumnFieldsMapById(url, opts?.getParams);
assert.deepEqual(
[...fieldsByColId.keys()],
Object.keys(expectedFieldsByColId),
"The updated table should have the expected columns"
);
for (const [colId, expectedFields] of Object.entries(expectedFieldsByColId)) {
assert.deepInclude(fieldsByColId.get(colId), expectedFields);
}
} }
};
const COLUMN_TO_UPDATE = { const COLUMN_TO_ADD = {
id: "A", id: "Foo",
fields: { fields: {
type: "Numeric", type: "Text",
colId: "NewA" label: "FooLabel",
} }
}; };
it('should create new columns', async function () { const COLUMN_TO_UPDATE = {
await checkPut([COLUMN_TO_ADD], {}, { id: "A",
A: {}, B: {}, C: {}, Foo: COLUMN_TO_ADD.fields fields: {
type: "Numeric",
colId: "NewA"
}
};
it('should create new columns', async function () {
await checkPut([COLUMN_TO_ADD], {}, {
A: {}, B: {}, C: {}, Foo: COLUMN_TO_ADD.fields
});
});
it('should update existing columns and create new ones', async function () {
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {}, {
NewA: {type: "Numeric", label: "A"}, B: {}, C: {}, Foo: COLUMN_TO_ADD.fields
});
});
it('should only update existing columns when noadd is set', async function () {
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {noadd: "1"}, {
NewA: {type: "Numeric"}, B: {}, C: {}
});
});
it('should only add columns when noupdate is set', async function () {
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {noupdate: "1"}, {
A: {type: "Any"}, B: {}, C: {}, Foo: COLUMN_TO_ADD.fields
});
});
it('should remove existing columns if replaceall is set', async function () {
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {replaceall: "1"}, {
NewA: {type: "Numeric"}, Foo: COLUMN_TO_ADD.fields
});
});
it('should NOT remove hidden columns even when replaceall is set', async function () {
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {replaceall: "1"}, {
manualSort: {type: "ManualSortPos"}, NewA: {type: "Numeric"}, Foo: COLUMN_TO_ADD.fields
}, { getParams: { hidden: true } });
});
it('should forbid update by viewers', async function () {
// given
const { url, docId } = await generateDocAndUrl('ColumnsPut');
await userApi.updateDocPermissions(docId, {users: {'kiwi@getgrist.com': 'viewers'}});
// when
const resp = await axios.put(url, { columns: [ COLUMN_TO_ADD ] }, kiwi);
// then
assert.equal(resp.status, 403);
});
it("should return 404 when table is not found", async function() {
// given
const { url } = await generateDocAndUrl('ColumnsPut');
const notFoundUrl = url.replace("Table1", "NonExistingTable");
// when
const resp = await axios.put(notFoundUrl, { columns: [ COLUMN_TO_ADD ] }, chimpy);
// then
assert.equal(resp.status, 404);
assert.equal(resp.data.error, 'Table not found "NonExistingTable"');
}); });
}); });
it('should update existing columns and create new ones', async function () { describe("DELETE /docs/{did}/tables/{tid}/columns/{colId}", function () {
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {}, { it('should delete some column', async function() {
NewA: {type: "Numeric", label: "A"}, B: {}, C: {}, Foo: COLUMN_TO_ADD.fields const {url} = await generateDocAndUrl('ColumnDelete');
const deleteUrl = url + '/A';
const resp = await axios.delete(deleteUrl, chimpy);
assert.equal(resp.status, 200, "Should succeed in requesting column deletion");
const listColResp = await axios.get(url, { ...chimpy, params: { hidden: true } });
assert.equal(listColResp.status, 200, "Should succeed in listing columns");
const columnIds = listColResp.data.columns.map(({id}: {id: string}) => id).sort();
assert.deepEqual(columnIds, ["B", "C", "manualSort"]);
}); });
});
it('should only update existing columns when noadd is set', async function () { it('should return 404 if table not found', async function() {
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {noadd: "1"}, { const {url} = await generateDocAndUrl('ColumnDelete');
NewA: {type: "Numeric"}, B: {}, C: {} const deleteUrl = url.replace("Table1", "NonExistingTable") + '/A';
const resp = await axios.delete(deleteUrl, chimpy);
assert.equal(resp.status, 404);
assert.equal(resp.data.error, 'Table or column not found "NonExistingTable.A"');
}); });
});
it('should only add columns when noupdate is set', async function () { it('should return 404 if column not found', async function() {
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {noupdate: "1"}, { const {url} = await generateDocAndUrl('ColumnDelete');
A: {type: "Any"}, B: {}, C: {}, Foo: COLUMN_TO_ADD.fields const deleteUrl = url + '/NonExistingColId';
const resp = await axios.delete(deleteUrl, chimpy);
assert.equal(resp.status, 404);
assert.equal(resp.data.error, 'Table or column not found "Table1.NonExistingColId"');
}); });
});
it('should remove existing columns if replaceall is set', async function () { it('should forbid column deletion by viewers', async function() {
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {replaceall: "1"}, { const {url, docId} = await generateDocAndUrl('ColumnDelete');
NewA: {type: "Numeric"}, Foo: COLUMN_TO_ADD.fields await userApi.updateDocPermissions(docId, {users: {'kiwi@getgrist.com': 'viewers'}});
const deleteUrl = url + '/A';
const resp = await axios.delete(deleteUrl, kiwi);
assert.equal(resp.status, 403);
}); });
}); });
it('should NOT remove hidden columns even when replaceall is set', async function () {
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {replaceall: "1"}, {
manualSort: {type: "ManualSortPos"}, NewA: {type: "Numeric"}, Foo: COLUMN_TO_ADD.fields
}, { getParams: { hidden: true } });
});
it('should forbid update by viewers', async function () {
// given
const { url, docId } = await generateDocAndUrl();
await userApi.updateDocPermissions(docId, {users: {'kiwi@getgrist.com': 'viewers'}});
// when
const resp = await axios.put(url, { columns: [ COLUMN_TO_ADD ] }, kiwi);
// then
assert.equal(resp.status, 403);
});
it("should return 404 when table is not found", async function() {
// given
const { url } = await generateDocAndUrl();
const notFoundUrl = url.replace("Table1", "NonExistingTable");
// when
const resp = await axios.put(notFoundUrl, { columns: [ COLUMN_TO_ADD ] }, chimpy);
// then
assert.equal(resp.status, 404);
assert.equal(resp.data.error, 'Table not found "NonExistingTable"');
});
}); });
it("GET /docs/{did}/tables/{tid}/data returns 404 for non-existent doc", async function () { it("GET /docs/{did}/tables/{tid}/data returns 404 for non-existent doc", async function () {