mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Add GET /attachments endpoint for listing attachment metadata
Summary: Combines the code and behaviour of the existing endpoints `GET /records` (for the general shape of the result and the parameters for sort/filter/limit etc) and retrieving a specific attachment with `GET /attachments/:id` for handling fields specific to attachments. Test Plan: Added a DocApi test. Also updated one test to use the new endpoint instead of raw `GET /tables/_grist_Attachments/records`. Reviewers: cyprien Reviewed By: cyprien Subscribers: cyprien Differential Revision: https://phab.getgrist.com/D3443
This commit is contained in:
parent
9bc04a6e66
commit
fcbad1c887
@ -6,6 +6,7 @@ import {isRaisedException} from "app/common/gristTypes";
|
|||||||
import {isAffirmative} from "app/common/gutil";
|
import {isAffirmative} from "app/common/gutil";
|
||||||
import {SortFunc} from 'app/common/SortFunc';
|
import {SortFunc} from 'app/common/SortFunc';
|
||||||
import {Sort} from 'app/common/SortSpec';
|
import {Sort} from 'app/common/SortSpec';
|
||||||
|
import {MetaRowRecord} from 'app/common/TableData';
|
||||||
import {DocReplacementOptions, DocState, DocStateComparison, DocStates, NEW_DOCUMENT_CODE} from 'app/common/UserAPI';
|
import {DocReplacementOptions, DocState, DocStateComparison, DocStates, NEW_DOCUMENT_CODE} from 'app/common/UserAPI';
|
||||||
import {HomeDBManager, makeDocAuthResult} from 'app/gen-server/lib/HomeDBManager';
|
import {HomeDBManager, makeDocAuthResult} from 'app/gen-server/lib/HomeDBManager';
|
||||||
import * as Types from "app/plugin/DocApiTypes";
|
import * as Types from "app/plugin/DocApiTypes";
|
||||||
@ -147,14 +148,14 @@ export class DocWorkerApi {
|
|||||||
res.json(await activeDoc.applyUserActions(docSessionFromRequest(req), req.body, {parseStrings}));
|
res.json(await activeDoc.applyUserActions(docSessionFromRequest(req), req.body, {parseStrings}));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
async function getTableData(activeDoc: ActiveDoc, req: RequestWithLogin) {
|
async function getTableData(activeDoc: ActiveDoc, req: RequestWithLogin, optTableId?: string) {
|
||||||
const filters = req.query.filter ? JSON.parse(String(req.query.filter)) : {};
|
const filters = req.query.filter ? JSON.parse(String(req.query.filter)) : {};
|
||||||
// Option to skip waiting for document initialization.
|
// Option to skip waiting for document initialization.
|
||||||
const immediate = isAffirmative(req.query.immediate);
|
const immediate = isAffirmative(req.query.immediate);
|
||||||
if (!Object.keys(filters).every(col => Array.isArray(filters[col]))) {
|
if (!Object.keys(filters).every(col => Array.isArray(filters[col]))) {
|
||||||
throw new ApiError("Invalid query: filter values must be arrays", 400);
|
throw new ApiError("Invalid query: filter values must be arrays", 400);
|
||||||
}
|
}
|
||||||
const tableId = req.params.tableId;
|
const tableId = optTableId || req.params.tableId;
|
||||||
const session = docSessionFromRequest(req);
|
const session = docSessionFromRequest(req);
|
||||||
const tableData = await handleSandboxError(tableId, [], activeDoc.fetchQuery(
|
const tableData = await handleSandboxError(tableId, [], activeDoc.fetchQuery(
|
||||||
session, {tableId, filters}, !immediate));
|
session, {tableId, filters}, !immediate));
|
||||||
@ -168,23 +169,16 @@ export class DocWorkerApi {
|
|||||||
return applyQueryParameters(fromTableDataAction(tableData), params, columns);
|
return applyQueryParameters(fromTableDataAction(tableData), params, columns);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the specified table in column-oriented format
|
async function getTableRecords(
|
||||||
this._app.get('/api/docs/:docId/tables/:tableId/data', canView,
|
activeDoc: ActiveDoc, req: RequestWithLogin, optTableId?: string
|
||||||
withDoc(async (activeDoc, req, res) => {
|
): Promise<TableRecordValue[]> {
|
||||||
res.json(await getTableData(activeDoc, req));
|
const columnData = await getTableData(activeDoc, req, optTableId);
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get the specified table in record-oriented format
|
|
||||||
this._app.get('/api/docs/:docId/tables/:tableId/records', canView,
|
|
||||||
withDoc(async (activeDoc, req, res) => {
|
|
||||||
const columnData = await getTableData(activeDoc, req);
|
|
||||||
const fieldNames = Object.keys(columnData)
|
const fieldNames = Object.keys(columnData)
|
||||||
.filter(k => !(
|
.filter(k => !(
|
||||||
["id", "manualSort"].includes(k)
|
["id", "manualSort"].includes(k)
|
||||||
|| k.startsWith("gristHelper_")
|
|| k.startsWith("gristHelper_")
|
||||||
));
|
));
|
||||||
const records = columnData.id.map((id, index) => {
|
return columnData.id.map((id, index) => {
|
||||||
const result: TableRecordValue = {id, fields: {}};
|
const result: TableRecordValue = {id, fields: {}};
|
||||||
for (const key of fieldNames) {
|
for (const key of fieldNames) {
|
||||||
let value = columnData[key][index];
|
let value = columnData[key][index];
|
||||||
@ -196,6 +190,19 @@ export class DocWorkerApi {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the specified table in column-oriented format
|
||||||
|
this._app.get('/api/docs/:docId/tables/:tableId/data', canView,
|
||||||
|
withDoc(async (activeDoc, req, res) => {
|
||||||
|
res.json(await getTableData(activeDoc, req));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the specified table in record-oriented format
|
||||||
|
this._app.get('/api/docs/:docId/tables/:tableId/records', canView,
|
||||||
|
withDoc(async (activeDoc, req, res) => {
|
||||||
|
const records = await getTableRecords(activeDoc, req);
|
||||||
res.json({records});
|
res.json({records});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -222,12 +229,28 @@ export class DocWorkerApi {
|
|||||||
res.json(await activeDoc.addAttachments(docSessionFromRequest(req), uploadResult.uploadId));
|
res.json(await activeDoc.addAttachments(docSessionFromRequest(req), uploadResult.uploadId));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Returns the metadata for a given attachment ID (i.e. a rowId in _grist_Attachments table).
|
// Select the fields from an attachment record that we want to return to the user,
|
||||||
|
// and convert the timeUploaded from a number to an ISO string.
|
||||||
|
function cleanAttachmentRecord(record: MetaRowRecord<"_grist_Attachments">) {
|
||||||
|
const {fileName, fileSize, timeUploaded: time} = record;
|
||||||
|
const timeUploaded = (typeof time === 'number') ? new Date(time).toISOString() : undefined;
|
||||||
|
return {fileName, fileSize, timeUploaded};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns cleaned metadata for all attachments in /records format.
|
||||||
|
this._app.get('/api/docs/:docId/attachments', canView, withDoc(async (activeDoc, req, res) => {
|
||||||
|
const rawRecords = await getTableRecords(activeDoc, req, "_grist_Attachments");
|
||||||
|
const records = rawRecords.map(r => ({
|
||||||
|
id: r.id,
|
||||||
|
fields: cleanAttachmentRecord(r.fields as MetaRowRecord<"_grist_Attachments">),
|
||||||
|
}));
|
||||||
|
res.json({records});
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Returns cleaned metadata for a given attachment ID (i.e. a rowId in _grist_Attachments table).
|
||||||
this._app.get('/api/docs/:docId/attachments/:attId', canView, withDoc(async (activeDoc, req, res) => {
|
this._app.get('/api/docs/:docId/attachments/:attId', canView, withDoc(async (activeDoc, req, res) => {
|
||||||
const attRecord = activeDoc.getAttachmentMetadata(req.params.attId as string);
|
const attRecord = activeDoc.getAttachmentMetadata(req.params.attId as string);
|
||||||
const {fileName, fileSize, timeUploaded: t} = attRecord;
|
res.json(cleanAttachmentRecord(attRecord));
|
||||||
const timeUploaded = (typeof t === 'number') ? new Date(t).toISOString() : undefined;
|
|
||||||
res.json({fileName, fileSize, timeUploaded});
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Responds with attachment contents, with suitable Content-Type and Content-Disposition.
|
// Responds with attachment contents, with suitable Content-Type and Content-Disposition.
|
||||||
|
@ -1460,6 +1460,23 @@ function testDocApi() {
|
|||||||
assert.deepEqual(resp.data, [3]);
|
assert.deepEqual(resp.data, [3]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("GET /docs/{did}/attachments lists attachment metadata", async function() {
|
||||||
|
// Test that the usual /records query parameters like sort and filter also work
|
||||||
|
const url = `${serverUrl}/api/docs/${docIds.TestDoc}/attachments?sort=-fileName&limit=2`;
|
||||||
|
const resp = await axios.get(url, chimpy);
|
||||||
|
assert.equal(resp.status, 200);
|
||||||
|
const {records} = resp.data;
|
||||||
|
for (const record of records) {
|
||||||
|
assert.match(record.fields.timeUploaded, /^\d{4}-\d{2}-\d{2}T/);
|
||||||
|
delete record.fields.timeUploaded;
|
||||||
|
}
|
||||||
|
assert.deepEqual(records, [
|
||||||
|
{id: 2, fields: {fileName: "world.jpg", fileSize: 6}},
|
||||||
|
{id: 3, fields: {fileName: "hello.png", fileSize: 6}},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("GET /docs/{did}/attachments/{id} returns attachment metadata", async function() {
|
it("GET /docs/{did}/attachments/{id} returns attachment metadata", async function() {
|
||||||
const resp = await axios.get(`${serverUrl}/api/docs/${docIds.TestDoc}/attachments/2`, chimpy);
|
const resp = await axios.get(`${serverUrl}/api/docs/${docIds.TestDoc}/attachments/2`, chimpy);
|
||||||
assert.equal(resp.status, 200);
|
assert.equal(resp.status, 200);
|
||||||
@ -1700,10 +1717,7 @@ function testDocApi() {
|
|||||||
assert.deepEqual(resp.data, [1, 2, 3]);
|
assert.deepEqual(resp.data, [1, 2, 3]);
|
||||||
|
|
||||||
async function checkAttachmentIds(ids: number[]) {
|
async function checkAttachmentIds(ids: number[]) {
|
||||||
resp = await axios.get(
|
resp = await axios.get(`${docUrl}/attachments`, chimpy);
|
||||||
`${docUrl}/tables/_grist_Attachments/records`,
|
|
||||||
chimpy,
|
|
||||||
);
|
|
||||||
assert.equal(resp.status, 200);
|
assert.equal(resp.status, 200);
|
||||||
assert.deepEqual(resp.data.records.map((r: any) => r.id), ids);
|
assert.deepEqual(resp.data.records.map((r: any) => r.id), ids);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user