|
|
@ -440,9 +440,9 @@ export class DocWorkerApi {
|
|
|
|
// Responds with attachment contents, with suitable Content-Type and Content-Disposition.
|
|
|
|
// Responds with attachment contents, with suitable Content-Type and Content-Disposition.
|
|
|
|
this._app.get('/api/docs/:docId/attachments/:attId/download', canView, withDoc(async (activeDoc, req, res) => {
|
|
|
|
this._app.get('/api/docs/:docId/attachments/:attId/download', canView, withDoc(async (activeDoc, req, res) => {
|
|
|
|
const attId = integerParam(req.params.attId, 'attId');
|
|
|
|
const attId = integerParam(req.params.attId, 'attId');
|
|
|
|
const tableId = optStringParam(req.params.tableId);
|
|
|
|
const tableId = optStringParam(req.params.tableId, 'tableId');
|
|
|
|
const colId = optStringParam(req.params.colId);
|
|
|
|
const colId = optStringParam(req.params.colId, 'colId');
|
|
|
|
const rowId = optIntegerParam(req.params.rowId);
|
|
|
|
const rowId = optIntegerParam(req.params.rowId, 'rowId');
|
|
|
|
if ((tableId || colId || rowId) && !(tableId && colId && rowId)) {
|
|
|
|
if ((tableId || colId || rowId) && !(tableId && colId && rowId)) {
|
|
|
|
throw new ApiError('define all of tableId, colId and rowId, or none.', 400);
|
|
|
|
throw new ApiError('define all of tableId, colId and rowId, or none.', 400);
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -720,8 +720,9 @@ export class DocWorkerApi {
|
|
|
|
const options = {
|
|
|
|
const options = {
|
|
|
|
add: !isAffirmative(req.query.noadd),
|
|
|
|
add: !isAffirmative(req.query.noadd),
|
|
|
|
update: !isAffirmative(req.query.noupdate),
|
|
|
|
update: !isAffirmative(req.query.noupdate),
|
|
|
|
onMany: stringParam(req.query.onmany || "first", "onmany",
|
|
|
|
onMany: stringParam(req.query.onmany || "first", "onmany", {
|
|
|
|
["first", "none", "all"]) as 'first'|'none'|'all'|undefined,
|
|
|
|
allowed: ["first", "none", "all"],
|
|
|
|
|
|
|
|
}) as 'first'|'none'|'all'|undefined,
|
|
|
|
allowEmptyRequire: isAffirmative(req.query.allow_empty_require),
|
|
|
|
allowEmptyRequire: isAffirmative(req.query.allow_empty_require),
|
|
|
|
};
|
|
|
|
};
|
|
|
|
await ops.upsert(body.records, options);
|
|
|
|
await ops.upsert(body.records, options);
|
|
|
@ -982,7 +983,7 @@ export class DocWorkerApi {
|
|
|
|
// (Requires a special permit.)
|
|
|
|
// (Requires a special permit.)
|
|
|
|
this._app.post('/api/docs/:docId/assign', canEdit, throttled(async (req, res) => {
|
|
|
|
this._app.post('/api/docs/:docId/assign', canEdit, throttled(async (req, res) => {
|
|
|
|
const docId = getDocId(req);
|
|
|
|
const docId = getDocId(req);
|
|
|
|
const group = optStringParam(req.query.group);
|
|
|
|
const group = optStringParam(req.query.group, 'group');
|
|
|
|
if (group !== undefined && req.specialPermit?.action === 'assign-doc') {
|
|
|
|
if (group !== undefined && req.specialPermit?.action === 'assign-doc') {
|
|
|
|
if (group.trim() === '') {
|
|
|
|
if (group.trim() === '') {
|
|
|
|
await this._docWorkerMap.removeDocGroup(docId);
|
|
|
|
await this._docWorkerMap.removeDocGroup(docId);
|
|
|
@ -1148,7 +1149,12 @@ export class DocWorkerApi {
|
|
|
|
const userId = getUserId(req);
|
|
|
|
const userId = getUserId(req);
|
|
|
|
const wsId = integerParam(req.params.wid, 'wid');
|
|
|
|
const wsId = integerParam(req.params.wid, 'wid');
|
|
|
|
const uploadId = integerParam(req.body.uploadId, 'uploadId');
|
|
|
|
const uploadId = integerParam(req.body.uploadId, 'uploadId');
|
|
|
|
const result = await this._docManager.importDocToWorkspace(userId, uploadId, wsId, req.body.browserSettings);
|
|
|
|
const result = await this._docManager.importDocToWorkspace({
|
|
|
|
|
|
|
|
userId,
|
|
|
|
|
|
|
|
uploadId,
|
|
|
|
|
|
|
|
workspaceId: wsId,
|
|
|
|
|
|
|
|
browserSettings: req.body.browserSettings,
|
|
|
|
|
|
|
|
});
|
|
|
|
res.json(result);
|
|
|
|
res.json(result);
|
|
|
|
}));
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
@ -1215,16 +1221,22 @@ export class DocWorkerApi {
|
|
|
|
})
|
|
|
|
})
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Create a document. When an upload is included, it is imported as the initial
|
|
|
|
/**
|
|
|
|
// state of the document. Otherwise a fresh empty document is created.
|
|
|
|
* Create a document.
|
|
|
|
// A "timezone" option can be supplied.
|
|
|
|
*
|
|
|
|
// Documents are created "unsaved".
|
|
|
|
* When an upload is included, it is imported as the initial state of the document.
|
|
|
|
// TODO: support workspaceId option for creating regular documents, at which point
|
|
|
|
* Otherwise, the document is left empty.
|
|
|
|
// existing import endpoint and doc creation endpoint can share implementation
|
|
|
|
*
|
|
|
|
// with this.
|
|
|
|
* If a workspace id is included, the document will be saved there instead of
|
|
|
|
// Returns the id of the created document.
|
|
|
|
* being left "unsaved".
|
|
|
|
|
|
|
|
*
|
|
|
|
|
|
|
|
* Returns the id of the created document.
|
|
|
|
|
|
|
|
*
|
|
|
|
|
|
|
|
* TODO: unify this with the other document creation and import endpoints.
|
|
|
|
|
|
|
|
*/
|
|
|
|
this._app.post('/api/docs', expressWrap(async (req, res) => {
|
|
|
|
this._app.post('/api/docs', expressWrap(async (req, res) => {
|
|
|
|
const userId = getUserId(req);
|
|
|
|
const userId = getUserId(req);
|
|
|
|
|
|
|
|
|
|
|
|
let uploadId: number|undefined;
|
|
|
|
let uploadId: number|undefined;
|
|
|
|
let parameters: {[key: string]: any};
|
|
|
|
let parameters: {[key: string]: any};
|
|
|
|
if (req.is('multipart/form-data')) {
|
|
|
|
if (req.is('multipart/form-data')) {
|
|
|
@ -1236,22 +1248,52 @@ export class DocWorkerApi {
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
parameters = req.body;
|
|
|
|
parameters = req.body;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (parameters.workspaceId) { throw new Error('workspaceId not supported'); }
|
|
|
|
|
|
|
|
|
|
|
|
const documentName = optStringParam(parameters.documentName, 'documentName', {
|
|
|
|
|
|
|
|
allowEmpty: false,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const workspaceId = optIntegerParam(parameters.workspaceId, 'workspaceId');
|
|
|
|
const browserSettings: BrowserSettings = {};
|
|
|
|
const browserSettings: BrowserSettings = {};
|
|
|
|
if (parameters.timezone) { browserSettings.timezone = parameters.timezone; }
|
|
|
|
if (parameters.timezone) { browserSettings.timezone = parameters.timezone; }
|
|
|
|
browserSettings.locale = localeFromRequest(req);
|
|
|
|
browserSettings.locale = localeFromRequest(req);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let docId: string;
|
|
|
|
if (uploadId !== undefined) {
|
|
|
|
if (uploadId !== undefined) {
|
|
|
|
const result = await this._docManager.importDocToWorkspace(userId, uploadId, null,
|
|
|
|
const result = await this._docManager.importDocToWorkspace({
|
|
|
|
browserSettings);
|
|
|
|
userId,
|
|
|
|
return res.json(result.id);
|
|
|
|
uploadId,
|
|
|
|
|
|
|
|
documentName,
|
|
|
|
|
|
|
|
workspaceId,
|
|
|
|
|
|
|
|
browserSettings,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
docId = result.id;
|
|
|
|
|
|
|
|
} else if (workspaceId !== undefined) {
|
|
|
|
|
|
|
|
const {status, data, errMessage} = await this._dbManager.addDocument(getScope(req), workspaceId, {
|
|
|
|
|
|
|
|
name: documentName ?? 'Untitled document',
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
if (status !== 200) {
|
|
|
|
|
|
|
|
throw new ApiError(errMessage || 'unable to create document', status);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docId = data!;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
const isAnonymous = isAnonymousUser(req);
|
|
|
|
|
|
|
|
const result = makeForkIds({
|
|
|
|
|
|
|
|
userId,
|
|
|
|
|
|
|
|
isAnonymous,
|
|
|
|
|
|
|
|
trunkDocId: NEW_DOCUMENT_CODE,
|
|
|
|
|
|
|
|
trunkUrlId: NEW_DOCUMENT_CODE,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
docId = result.docId;
|
|
|
|
|
|
|
|
await this._docManager.createNamedDoc(
|
|
|
|
|
|
|
|
makeExceptionalDocSession('nascent', {
|
|
|
|
|
|
|
|
req: req as RequestWithLogin,
|
|
|
|
|
|
|
|
browserSettings,
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
docId
|
|
|
|
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const isAnonymous = isAnonymousUser(req);
|
|
|
|
|
|
|
|
const {docId} = makeForkIds({userId, isAnonymous, trunkDocId: NEW_DOCUMENT_CODE,
|
|
|
|
|
|
|
|
trunkUrlId: NEW_DOCUMENT_CODE});
|
|
|
|
|
|
|
|
await this._docManager.createNamedDoc(makeExceptionalDocSession('nascent', {
|
|
|
|
|
|
|
|
req: req as RequestWithLogin,
|
|
|
|
|
|
|
|
browserSettings
|
|
|
|
|
|
|
|
}), docId);
|
|
|
|
|
|
|
|
return res.status(200).json(docId);
|
|
|
|
return res.status(200).json(docId);
|
|
|
|
}));
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -1556,7 +1598,7 @@ export class DocWorkerApi {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const timeout =
|
|
|
|
const timeout =
|
|
|
|
Math.max(0, Math.min(MAX_CUSTOM_SQL_MSEC,
|
|
|
|
Math.max(0, Math.min(MAX_CUSTOM_SQL_MSEC,
|
|
|
|
optIntegerParam(options.timeout) || MAX_CUSTOM_SQL_MSEC));
|
|
|
|
optIntegerParam(options.timeout, 'timeout') || MAX_CUSTOM_SQL_MSEC));
|
|
|
|
// Wrap in a select to commit to the SELECT branch of SQLite
|
|
|
|
// Wrap in a select to commit to the SELECT branch of SQLite
|
|
|
|
// grammar. Note ; isn't a problem.
|
|
|
|
// grammar. Note ; isn't a problem.
|
|
|
|
//
|
|
|
|
//
|
|
|
@ -1639,7 +1681,7 @@ export interface QueryParameters {
|
|
|
|
* as a header.
|
|
|
|
* as a header.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
function getSortParameter(req: Request): string[]|undefined {
|
|
|
|
function getSortParameter(req: Request): string[]|undefined {
|
|
|
|
const sortString: string|undefined = optStringParam(req.query.sort) || req.get('X-Sort');
|
|
|
|
const sortString: string|undefined = optStringParam(req.query.sort, 'sort') || req.get('X-Sort');
|
|
|
|
if (!sortString) { return undefined; }
|
|
|
|
if (!sortString) { return undefined; }
|
|
|
|
return sortString.split(',');
|
|
|
|
return sortString.split(',');
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -1650,7 +1692,7 @@ function getSortParameter(req: Request): string[]|undefined {
|
|
|
|
* parameter, or as a header.
|
|
|
|
* parameter, or as a header.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
function getLimitParameter(req: Request): number|undefined {
|
|
|
|
function getLimitParameter(req: Request): number|undefined {
|
|
|
|
const limitString: string|undefined = optStringParam(req.query.limit) || req.get('X-Limit');
|
|
|
|
const limitString: string|undefined = optStringParam(req.query.limit, 'limit') || req.get('X-Limit');
|
|
|
|
if (!limitString) { return undefined; }
|
|
|
|
if (!limitString) { return undefined; }
|
|
|
|
const limit = parseInt(limitString, 10);
|
|
|
|
const limit = parseInt(limitString, 10);
|
|
|
|
if (isNaN(limit)) { throw new Error('limit is not a number'); }
|
|
|
|
if (isNaN(limit)) { throw new Error('limit is not a number'); }
|
|
|
|