chore: API Consistency (#1304)

* chore: Addressing API inconsistencies

* lint

* add: Missing sort to groups.list
fix: Documention issues

* test: fix

* feat: Add missing shares.info endpoint

* feat: Add sorting to users.list endpoint

* fix: Incorrect pagination parameters listed on user endpoints

* users.s3Upload -> attachments.create

* chore: exportAll -> export_all
This commit is contained in:
Tom Moor
2020-06-16 20:56:17 -07:00
committed by GitHub
parent 5010b08e83
commit 0f8d503df8
20 changed files with 309 additions and 250 deletions

View File

@ -42,7 +42,7 @@ class DocumentHistory extends React.Component<Props> {
const results = await this.props.revisions.fetchPage({ const results = await this.props.revisions.fetchPage({
limit, limit,
offset: this.offset, offset: this.offset,
id: this.props.match.params.documentSlug, documentId: this.props.match.params.documentSlug,
}); });
if ( if (

View File

@ -267,7 +267,7 @@ class CollectionScene extends React.Component<Props> {
collection.id collection.id
)} )}
fetch={documents.fetchAlphabetical} fetch={documents.fetchAlphabetical}
options={{ collection: collection.id }} options={{ collectionId: collection.id }}
showPin showPin
/> />
</Route> </Route>
@ -278,7 +278,7 @@ class CollectionScene extends React.Component<Props> {
collection.id collection.id
)} )}
fetch={documents.fetchLeastRecentlyUpdated} fetch={documents.fetchLeastRecentlyUpdated}
options={{ collection: collection.id }} options={{ collectionId: collection.id }}
showPin showPin
/> />
</Route> </Route>
@ -289,7 +289,7 @@ class CollectionScene extends React.Component<Props> {
collection.id collection.id
)} )}
fetch={documents.fetchRecentlyPublished} fetch={documents.fetchRecentlyPublished}
options={{ collection: collection.id }} options={{ collectionId: collection.id }}
showPublished showPublished
showPin showPin
/> />
@ -300,7 +300,7 @@ class CollectionScene extends React.Component<Props> {
collection.id collection.id
)} )}
fetch={documents.fetchRecentlyUpdated} fetch={documents.fetchRecentlyUpdated}
options={{ collection: collection.id }} options={{ collectionId: collection.id }}
showPin showPin
/> />
</Route> </Route>

View File

@ -97,11 +97,8 @@ class DataLoader extends React.Component<Props> {
}; };
loadRevision = async () => { loadRevision = async () => {
const { documentSlug, revisionId } = this.props.match.params; const { revisionId } = this.props.match.params;
this.revision = await this.props.revisions.fetch(revisionId);
this.revision = await this.props.revisions.fetch(documentSlug, {
revisionId,
});
}; };
loadDocument = async () => { loadDocument = async () => {

View File

@ -105,6 +105,6 @@ export default class CollectionsStore extends BaseStore<Collection> {
} }
export = () => { export = () => {
return client.post('/collections.exportAll'); return client.post('/collections.export_all');
}; };
} }

View File

@ -20,21 +20,16 @@ export default class RevisionsStore extends BaseStore<Revision> {
} }
@action @action
fetch = async ( fetch = async (id: string, options?: FetchOptions): Promise<?Revision> => {
documentId: string,
options?: FetchOptions
): Promise<?Revision> => {
this.isFetching = true; this.isFetching = true;
const id = options && options.revisionId; invariant(id, 'Id is required');
if (!id) throw new Error('revisionId is required');
try { try {
const rev = this.data.get(id); const rev = this.data.get(id);
if (rev) return rev; if (rev) return rev;
const res = await client.post('/documents.revision', { const res = await client.post('/revisions.info', {
id: documentId, id,
revisionId: id,
}); });
invariant(res && res.data, 'Revision not available'); invariant(res && res.data, 'Revision not available');
this.add(res.data); this.add(res.data);
@ -54,7 +49,7 @@ export default class RevisionsStore extends BaseStore<Revision> {
this.isFetching = true; this.isFetching = true;
try { try {
const res = await client.post('/documents.revisions', options); const res = await client.post('/revisions.list', options);
invariant(res && res.data, 'Document revisions not available'); invariant(res && res.data, 'Document revisions not available');
runInAction('RevisionsStore#fetchPage', () => { runInAction('RevisionsStore#fetchPage', () => {
res.data.forEach(revision => this.add(revision)); res.data.forEach(revision => this.add(revision));

View File

@ -13,7 +13,7 @@ export const uploadFile = async (
options?: Options = { name: '' } options?: Options = { name: '' }
) => { ) => {
const name = file instanceof File ? file.name : options.name; const name = file instanceof File ? file.name : options.name;
const response = await client.post('/users.s3Upload', { const response = await client.post('/attachments.create', {
public: options.public, public: options.public,
documentId: options.documentId, documentId: options.documentId,
contentType: file.type, contentType: file.type,
@ -24,7 +24,7 @@ export const uploadFile = async (
invariant(response, 'Response should be available'); invariant(response, 'Response should be available');
const data = response.data; const data = response.data;
const asset = data.asset; const attachment = data.attachment;
const formData = new FormData(); const formData = new FormData();
for (const key in data.form) { for (const key in data.form) {
@ -44,7 +44,7 @@ export const uploadFile = async (
body: formData, body: formData,
}); });
return asset; return attachment;
}; };
export const dataUrlToBlob = (dataURL: string) => { export const dataUrlToBlob = (dataURL: string) => {

View File

@ -43,7 +43,7 @@ Object {
} }
`; `;
exports[`#collections.exportAll should require authentication 1`] = ` exports[`#collections.export_all should require authentication 1`] = `
Object { Object {
"error": "authentication_required", "error": "authentication_required",
"message": "Authentication required", "message": "Authentication required",

View File

@ -1,13 +1,92 @@
// @flow // @flow
import Router from 'koa-router'; import Router from 'koa-router';
import uuid from 'uuid';
import format from 'date-fns/format';
import { Attachment, Document, Event } from '../models';
import {
makePolicy,
getSignature,
publicS3Endpoint,
makeCredential,
getSignedImageUrl,
} from '../utils/s3';
import auth from '../middlewares/authentication'; import auth from '../middlewares/authentication';
import { Attachment, Document } from '../models';
import { getSignedImageUrl } from '../utils/s3';
import { NotFoundError } from '../errors'; import { NotFoundError } from '../errors';
import policy from '../policies'; import policy from '../policies';
const { authorize } = policy; const { authorize } = policy;
const router = new Router(); const router = new Router();
const AWS_S3_ACL = process.env.AWS_S3_ACL || 'private';
router.post('attachments.create', auth(), async ctx => {
let { name, documentId, contentType, size } = ctx.body;
ctx.assertPresent(name, 'name is required');
ctx.assertPresent(contentType, 'contentType is required');
ctx.assertPresent(size, 'size is required');
const { user } = ctx.state;
const s3Key = uuid.v4();
const key = `uploads/${user.id}/${s3Key}/${name}`;
const acl =
ctx.body.public === undefined
? AWS_S3_ACL
: ctx.body.public ? 'public-read' : 'private';
const credential = makeCredential();
const longDate = format(new Date(), 'YYYYMMDDTHHmmss\\Z');
const policy = makePolicy(credential, longDate, acl);
const endpoint = publicS3Endpoint();
const url = `${endpoint}/${key}`;
if (documentId) {
const document = await Document.findByPk(documentId, { userId: user.id });
authorize(user, 'update', document);
}
const attachment = await Attachment.create({
key,
acl,
size,
url,
contentType,
documentId,
teamId: user.teamId,
userId: user.id,
});
await Event.create({
name: 'attachments.create',
data: { name },
teamId: user.teamId,
userId: user.id,
ip: ctx.request.ip,
});
ctx.body = {
data: {
maxUploadSize: process.env.AWS_S3_UPLOAD_MAX_SIZE,
uploadUrl: endpoint,
form: {
'Cache-Control': 'max-age=31557600',
'Content-Type': contentType,
acl,
key,
policy,
'x-amz-algorithm': 'AWS4-HMAC-SHA256',
'x-amz-credential': credential,
'x-amz-date': longDate,
'x-amz-signature': getSignature(policy),
},
attachment: {
documentId,
contentType,
name,
url: attachment.redirectUrl,
size,
},
},
};
});
router.post('attachments.redirect', auth(), async ctx => { router.post('attachments.redirect', auth(), async ctx => {
const { id } = ctx.body; const { id } = ctx.body;

View File

@ -410,7 +410,7 @@ router.post('collections.export', auth(), async ctx => {
ctx.body = fs.createReadStream(filePath); ctx.body = fs.createReadStream(filePath);
}); });
router.post('collections.exportAll', auth(), async ctx => { router.post('collections.export_all', auth(), async ctx => {
const { download = false } = ctx.body; const { download = false } = ctx.body;
const user = ctx.state.user; const user = ctx.state.user;

View File

@ -176,9 +176,9 @@ describe('#collections.export', async () => {
}); });
}); });
describe('#collections.exportAll', async () => { describe('#collections.export_all', async () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const res = await server.post('/api/collections.exportAll'); const res = await server.post('/api/collections.export_all');
const body = await res.json(); const body = await res.json();
expect(res.status).toEqual(401); expect(res.status).toEqual(401);
@ -187,7 +187,7 @@ describe('#collections.exportAll', async () => {
it('should require authorization', async () => { it('should require authorization', async () => {
const user = await buildUser(); const user = await buildUser();
const res = await server.post('/api/collections.exportAll', { const res = await server.post('/api/collections.export_all', {
body: { token: user.getJwtToken() }, body: { token: user.getJwtToken() },
}); });
expect(res.status).toEqual(403); expect(res.status).toEqual(403);
@ -195,7 +195,7 @@ describe('#collections.exportAll', async () => {
it('should return success', async () => { it('should return success', async () => {
const { admin } = await seed(); const { admin } = await seed();
const res = await server.post('/api/collections.exportAll', { const res = await server.post('/api/collections.export_all', {
body: { token: admin.getJwtToken() }, body: { token: admin.getJwtToken() },
}); });
@ -204,7 +204,7 @@ describe('#collections.exportAll', async () => {
it('should allow downloading directly', async () => { it('should allow downloading directly', async () => {
const { admin } = await seed(); const { admin } = await seed();
const res = await server.post('/api/collections.exportAll', { const res = await server.post('/api/collections.export_all', {
body: { token: admin.getJwtToken(), download: true }, body: { token: admin.getJwtToken(), download: true },
}); });

View File

@ -7,7 +7,6 @@ import documentMover from '../commands/documentMover';
import { import {
presentDocument, presentDocument,
presentCollection, presentCollection,
presentRevision,
presentPolicies, presentPolicies,
} from '../presenters'; } from '../presenters';
import { import {
@ -31,8 +30,10 @@ const router = new Router();
router.post('documents.list', auth(), pagination(), async ctx => { router.post('documents.list', auth(), pagination(), async ctx => {
const { sort = 'updatedAt', backlinkDocumentId, parentDocumentId } = ctx.body; const { sort = 'updatedAt', backlinkDocumentId, parentDocumentId } = ctx.body;
const collectionId = ctx.body.collection;
const createdById = ctx.body.user; // collection and user are here for backwards compatablity
const collectionId = ctx.body.collectionId || ctx.body.collection;
const createdById = ctx.body.userId || ctx.body.user;
let direction = ctx.body.direction; let direction = ctx.body.direction;
if (direction !== 'ASC') direction = 'DESC'; if (direction !== 'ASC') direction = 'DESC';
@ -411,54 +412,6 @@ router.post('documents.info', auth({ required: false }), async ctx => {
}; };
}); });
router.post('documents.revision', auth(), async ctx => {
let { id, revisionId } = ctx.body;
ctx.assertPresent(id, 'id is required');
ctx.assertPresent(revisionId, 'revisionId is required');
const user = ctx.state.user;
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'read', document);
const revision = await Revision.findOne({
where: {
id: revisionId,
documentId: document.id,
},
});
ctx.body = {
pagination: ctx.state.pagination,
data: await presentRevision(revision),
};
});
router.post('documents.revisions', auth(), pagination(), async ctx => {
let { id, sort = 'updatedAt', direction } = ctx.body;
if (direction !== 'ASC') direction = 'DESC';
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'read', document);
const revisions = await Revision.findAll({
where: { documentId: document.id },
order: [[sort, direction]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
const data = await Promise.all(
revisions.map(revision => presentRevision(revision))
);
ctx.body = {
pagination: ctx.state.pagination,
data,
};
});
router.post('documents.restore', auth(), async ctx => { router.post('documents.restore', auth(), async ctx => {
const { id, revisionId } = ctx.body; const { id, revisionId } = ctx.body;
ctx.assertPresent(id, 'id is required'); ctx.assertPresent(id, 'id is required');
@ -667,6 +620,10 @@ router.post('documents.star', auth(), async ctx => {
data: { title: document.title }, data: { title: document.title },
ip: ctx.request.ip, ip: ctx.request.ip,
}); });
ctx.body = {
success: true,
};
}); });
router.post('documents.unstar', auth(), async ctx => { router.post('documents.unstar', auth(), async ctx => {
@ -690,6 +647,10 @@ router.post('documents.unstar', auth(), async ctx => {
data: { title: document.title }, data: { title: document.title },
ip: ctx.request.ip, ip: ctx.request.ip,
}); });
ctx.body = {
success: true,
};
}); });
router.post('documents.create', auth(), async ctx => { router.post('documents.create', auth(), async ctx => {
@ -925,8 +886,8 @@ router.post('documents.move', auth(), async ctx => {
collections: await Promise.all( collections: await Promise.all(
collections.map(collection => presentCollection(collection)) collections.map(collection => presentCollection(collection))
), ),
policies: presentPolicies(user, documents),
}, },
policies: presentPolicies(user, documents),
}; };
}); });

View File

@ -436,48 +436,6 @@ describe('#documents.drafts', async () => {
}); });
}); });
describe('#documents.revision', async () => {
it("should return a document's revisions", async () => {
const { user, document } = await seed();
const res = await server.post('/api/documents.revisions', {
body: {
token: user.getJwtToken(),
id: document.id,
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.length).toEqual(1);
expect(body.data[0].id).not.toEqual(document.id);
expect(body.data[0].title).toEqual(document.title);
});
it('should not return revisions for document in collection not a member of', async () => {
const { user, document, collection } = await seed();
collection.private = true;
await collection.save();
const res = await server.post('/api/documents.revisions', {
body: { token: user.getJwtToken(), id: document.id },
});
expect(res.status).toEqual(403);
});
it('should require authorization', async () => {
const { document } = await seed();
const user = await buildUser();
const res = await server.post('/api/documents.revisions', {
body: {
token: user.getJwtToken(),
id: document.id,
},
});
expect(res.status).toEqual(403);
});
});
describe('#documents.search', async () => { describe('#documents.search', async () => {
it('should return results', async () => { it('should return results', async () => {
const { user } = await seed(); const { user } = await seed();

View File

@ -18,13 +18,16 @@ const { authorize } = policy;
const router = new Router(); const router = new Router();
router.post('groups.list', auth(), pagination(), async ctx => { router.post('groups.list', auth(), pagination(), async ctx => {
const { sort = 'updatedAt' } = ctx.body;
let direction = ctx.body.direction;
if (direction !== 'ASC') direction = 'DESC';
const user = ctx.state.user; const user = ctx.state.user;
let groups = await Group.findAll({ let groups = await Group.findAll({
where: { where: {
teamId: user.teamId, teamId: user.teamId,
}, },
order: [['updatedAt', 'DESC']], order: [[sort, direction]],
offset: ctx.state.pagination.offset, offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit, limit: ctx.state.pagination.limit,
}); });

View File

@ -8,6 +8,7 @@ import events from './events';
import users from './users'; import users from './users';
import collections from './collections'; import collections from './collections';
import documents from './documents'; import documents from './documents';
import revisions from './revisions';
import views from './views'; import views from './views';
import hooks from './hooks'; import hooks from './hooks';
import apiKeys from './apiKeys'; import apiKeys from './apiKeys';
@ -45,6 +46,7 @@ router.use('/', events.routes());
router.use('/', users.routes()); router.use('/', users.routes());
router.use('/', collections.routes()); router.use('/', collections.routes());
router.use('/', documents.routes()); router.use('/', documents.routes());
router.use('/', revisions.routes());
router.use('/', views.routes()); router.use('/', views.routes());
router.use('/', hooks.routes()); router.use('/', hooks.routes());
router.use('/', apiKeys.routes()); router.use('/', apiKeys.routes());

60
server/api/revisions.js Normal file
View File

@ -0,0 +1,60 @@
// @flow
import Router from 'koa-router';
import auth from '../middlewares/authentication';
import pagination from './middlewares/pagination';
import { presentRevision } from '../presenters';
import { Document, Revision } from '../models';
import { NotFoundError } from '../errors';
import policy from '../policies';
const { authorize } = policy;
const router = new Router();
router.post('revisions.info', auth(), async ctx => {
let { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const revision = await Revision.findByPk(id);
if (!revision) {
throw new NotFoundError();
}
const document = await Document.findByPk(revision.documentId, {
userId: user.id,
});
authorize(user, 'read', document);
ctx.body = {
pagination: ctx.state.pagination,
data: await presentRevision(revision),
};
});
router.post('revisions.list', auth(), pagination(), async ctx => {
let { documentId, sort = 'updatedAt', direction } = ctx.body;
if (direction !== 'ASC') direction = 'DESC';
ctx.assertPresent(documentId, 'documentId is required');
const user = ctx.state.user;
const document = await Document.findByPk(documentId, { userId: user.id });
authorize(user, 'read', document);
const revisions = await Revision.findAll({
where: { documentId: document.id },
order: [[sort, direction]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
const data = await Promise.all(
revisions.map(revision => presentRevision(revision))
);
ctx.body = {
pagination: ctx.state.pagination,
data,
};
});
export default router;

View File

@ -0,0 +1,92 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server';
import app from '../app';
import { flushdb, seed } from '../test/support';
import { buildDocument, buildUser } from '../test/factories';
import Revision from '../models/Revision';
const server = new TestServer(app.callback());
beforeEach(flushdb);
afterAll(server.close);
describe('#revisions.info', async () => {
it('should return a document revision', async () => {
const { user, document } = await seed();
const revision = await Revision.findOne({
where: {
documentId: document.id,
},
});
const res = await server.post('/api/revisions.info', {
body: {
token: user.getJwtToken(),
id: revision.id,
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.id).not.toEqual(document.id);
expect(body.data.title).toEqual(document.title);
});
it('should require authorization', async () => {
const document = await buildDocument();
const revision = await Revision.findOne({
where: {
documentId: document.id,
},
});
const user = await buildUser();
const res = await server.post('/api/revisions.info', {
body: {
token: user.getJwtToken(),
id: revision.id,
},
});
expect(res.status).toEqual(403);
});
});
describe('#revisions.list', async () => {
it("should return a document's revisions", async () => {
const { user, document } = await seed();
const res = await server.post('/api/revisions.list', {
body: {
token: user.getJwtToken(),
documentId: document.id,
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.length).toEqual(1);
expect(body.data[0].id).not.toEqual(document.id);
expect(body.data[0].title).toEqual(document.title);
});
it('should not return revisions for document in collection not a member of', async () => {
const { user, document, collection } = await seed();
collection.private = true;
await collection.save();
const res = await server.post('/api/revisions.list', {
body: { token: user.getJwtToken(), documentId: document.id },
});
expect(res.status).toEqual(403);
});
it('should require authorization', async () => {
const document = await buildDocument();
const user = await buildUser();
const res = await server.post('/api/revisions.list', {
body: {
token: user.getJwtToken(),
documentId: document.id,
},
});
expect(res.status).toEqual(403);
});
});

View File

@ -11,6 +11,19 @@ const Op = Sequelize.Op;
const { authorize } = policy; const { authorize } = policy;
const router = new Router(); const router = new Router();
router.post('shares.info', auth(), async ctx => {
const { id } = ctx.body;
ctx.assertUuid(id, 'id is required');
const user = ctx.state.user;
const share = await Share.findByPk(id);
authorize(user, 'read', share);
ctx.body = {
data: presentShare(share),
};
});
router.post('shares.list', auth(), pagination(), async ctx => { router.post('shares.list', auth(), pagination(), async ctx => {
let { sort = 'updatedAt', direction } = ctx.body; let { sort = 'updatedAt', direction } = ctx.body;
if (direction !== 'ASC') direction = 'DESC'; if (direction !== 'ASC') direction = 'DESC';

View File

@ -1,27 +1,20 @@
// @flow // @flow
import uuid from 'uuid';
import Router from 'koa-router'; import Router from 'koa-router';
import format from 'date-fns/format';
import { Op } from '../sequelize'; import { Op } from '../sequelize';
import { import { Event, User, Team } from '../models';
makePolicy,
getSignature,
publicS3Endpoint,
makeCredential,
} from '../utils/s3';
import { Document, Attachment, Event, User, Team } from '../models';
import auth from '../middlewares/authentication'; import auth from '../middlewares/authentication';
import pagination from './middlewares/pagination'; import pagination from './middlewares/pagination';
import userInviter from '../commands/userInviter'; import userInviter from '../commands/userInviter';
import { presentUser } from '../presenters'; import { presentUser } from '../presenters';
import policy from '../policies'; import policy from '../policies';
const AWS_S3_ACL = process.env.AWS_S3_ACL || 'private';
const { authorize } = policy; const { authorize } = policy;
const router = new Router(); const router = new Router();
router.post('users.list', auth(), pagination(), async ctx => { router.post('users.list', auth(), pagination(), async ctx => {
const { query, includeSuspended = false } = ctx.body; const { sort = 'createdAt', query, includeSuspended = false } = ctx.body;
let direction = ctx.body.direction;
if (direction !== 'ASC') direction = 'DESC';
const user = ctx.state.user; const user = ctx.state.user;
let where = { let where = {
@ -48,7 +41,7 @@ router.post('users.list', auth(), pagination(), async ctx => {
const users = await User.findAll({ const users = await User.findAll({
where, where,
order: [['createdAt', 'DESC']], order: [[sort, direction]],
offset: ctx.state.pagination.offset, offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit, limit: ctx.state.pagination.limit,
}); });
@ -81,79 +74,6 @@ router.post('users.update', auth(), async ctx => {
}; };
}); });
router.post('users.s3Upload', auth(), async ctx => {
let { name, filename, documentId, contentType, kind, size } = ctx.body;
// backwards compatability
name = name || filename;
contentType = contentType || kind;
ctx.assertPresent(name, 'name is required');
ctx.assertPresent(contentType, 'contentType is required');
ctx.assertPresent(size, 'size is required');
const { user } = ctx.state;
const s3Key = uuid.v4();
const key = `uploads/${user.id}/${s3Key}/${name}`;
const acl =
ctx.body.public === undefined
? AWS_S3_ACL
: ctx.body.public ? 'public-read' : 'private';
const credential = makeCredential();
const longDate = format(new Date(), 'YYYYMMDDTHHmmss\\Z');
const policy = makePolicy(credential, longDate, acl);
const endpoint = publicS3Endpoint();
const url = `${endpoint}/${key}`;
if (documentId) {
const document = await Document.findByPk(documentId, { userId: user.id });
authorize(user, 'update', document);
}
const attachment = await Attachment.create({
key,
acl,
size,
url,
contentType,
documentId,
teamId: user.teamId,
userId: user.id,
});
await Event.create({
name: 'user.s3Upload',
data: { name },
teamId: user.teamId,
userId: user.id,
ip: ctx.request.ip,
});
ctx.body = {
data: {
maxUploadSize: process.env.AWS_S3_UPLOAD_MAX_SIZE,
uploadUrl: endpoint,
form: {
'Cache-Control': 'max-age=31557600',
'Content-Type': contentType,
acl,
key,
policy,
'x-amz-algorithm': 'AWS4-HMAC-SHA256',
'x-amz-credential': credential,
'x-amz-date': longDate,
'x-amz-signature': getSignature(policy),
},
asset: {
contentType,
name,
url: attachment.redirectUrl,
size,
},
},
};
});
// Admin specific // Admin specific
router.post('users.promote', auth(), async ctx => { router.post('users.promote', auth(), async ctx => {

View File

@ -49,7 +49,7 @@ export default function Api() {
</Arguments> </Arguments>
</Method> </Method>
<Method method="users.s3Upload" label="Get S3 upload credentials"> <Method method="attachments.create" label="Get S3 upload credentials">
<Description> <Description>
You can upload small files and images as part of your documents. You can upload small files and images as part of your documents.
All files are stored using Amazon S3. Instead of uploading files All files are stored using Amazon S3. Instead of uploading files
@ -84,7 +84,7 @@ export default function Api() {
Promote a user to be a team admin. This endpoint is only available Promote a user to be a team admin. This endpoint is only available
for admin users. for admin users.
</Description> </Description>
<Arguments pagination> <Arguments>
<Argument id="id" description="User ID to be promoted" required /> <Argument id="id" description="User ID to be promoted" required />
</Arguments> </Arguments>
</Method> </Method>
@ -95,7 +95,7 @@ export default function Api() {
is always required. This endpoint is only available for admin is always required. This endpoint is only available for admin
users. users.
</Description> </Description>
<Arguments pagination> <Arguments>
<Argument id="id" description="User ID to be demoted" required /> <Argument id="id" description="User ID to be demoted" required />
</Arguments> </Arguments>
</Method> </Method>
@ -105,7 +105,7 @@ export default function Api() {
Admin can suspend users to reduce the number of accounts on their Admin can suspend users to reduce the number of accounts on their
billing plan or prevent them from accessing documention. billing plan or prevent them from accessing documention.
</Description> </Description>
<Arguments pagination> <Arguments>
<Argument <Argument
id="id" id="id"
description="User ID to be suspended" description="User ID to be suspended"
@ -122,7 +122,7 @@ export default function Api() {
Admin can re-active a suspended user. This will update the billing Admin can re-active a suspended user. This will update the billing
plan and re-enable their access to the documention. plan and re-enable their access to the documention.
</Description> </Description>
<Arguments pagination> <Arguments>
<Argument <Argument
id="id" id="id"
description="User ID to be activated" description="User ID to be activated"
@ -173,7 +173,10 @@ export default function Api() {
</Arguments> </Arguments>
</Method> </Method>
<Method method="collections.exportAll" label="Export all collections"> <Method
method="collections.export_all"
label="Export all collections"
>
<Description> <Description>
Returns a zip file of all the collections or creates an async job Returns a zip file of all the collections or creates an async job
to send a zip file via email to the authenticated user. If to send a zip file via email to the authenticated user. If
@ -527,20 +530,6 @@ export default function Api() {
</Arguments> </Arguments>
</Method> </Method>
<Method method="documents.info" label="Get a document">
<Description>
Get a document with its ID or URL identifier from users
collections.
</Description>
<Arguments>
<Argument
id="id"
description="Document ID or URI identifier"
required
/>
</Arguments>
</Method>
<Method <Method
method="documents.restore" method="documents.restore"
label="Restore a previous revision" label="Restore a previous revision"
@ -651,32 +640,21 @@ export default function Api() {
</Arguments> </Arguments>
</Method> </Method>
<Method <Method method="revisions.info" label="Get revision for a document">
method="documents.revision"
label="Get revision for a document"
>
<Description>Return a specific revision of a document.</Description> <Description>Return a specific revision of a document.</Description>
<Arguments> <Arguments>
<Argument <Argument id="id" description="Revision ID" required />
id="id"
description="Document ID or URI identifier"
required
/>
<Argument id="revisionId" description="Revision ID" required />
</Arguments> </Arguments>
</Method> </Method>
<Method <Method method="revisions.list" label="Get revisions for a document">
method="documents.revisions"
label="Get revisions for a document"
>
<Description> <Description>
Return revisions for a document. Upon each edit, a new revision is Return revisions for a document. Upon each edit, a new revision is
stored. stored.
</Description> </Description>
<Arguments pagination> <Arguments pagination>
<Argument <Argument
id="id" id="documentId"
description="Document ID or URI identifier" description="Document ID or URI identifier"
required required
/> />
@ -688,7 +666,7 @@ export default function Api() {
This method allows you to create a new group to organize people in This method allows you to create a new group to organize people in
the team. the team.
</Description> </Description>
<Arguments pagination> <Arguments>
<Argument <Argument
id="name" id="name"
description="The name of the group" description="The name of the group"
@ -702,7 +680,7 @@ export default function Api() {
This method allows you to update an existing group. At this time This method allows you to update an existing group. At this time
the only field that can be edited is the name. the only field that can be edited is the name.
</Description> </Description>
<Arguments pagination> <Arguments>
<Argument id="id" description="Group ID" required /> <Argument id="id" description="Group ID" required />
<Argument <Argument
id="name" id="name"

View File

@ -6,6 +6,7 @@ export default function present(group: Group) {
id: group.id, id: group.id,
name: group.name, name: group.name,
memberCount: group.groupMemberships.length, memberCount: group.groupMemberships.length,
createdAt: group.createdAt,
updatedAt: group.updatedAt, updatedAt: group.updatedAt,
}; };
} }