Validate API request query (#4642)
* fix: refactor to accommodate authentication, transaction and pagination together into ctx.state * feat: allow passing response type to APIContext * feat: preliminary work for initial review * fix: use unknown for base types * fix: api/attachments * fix: api/documents * fix: jsdoc comment for input * fix: replace at() with index access for compatibility * fix: validation err message * fix: error handling * fix: remove unnecessary extend
This commit is contained in:
@@ -2,6 +2,7 @@ import { isEmpty } from "lodash";
|
||||
import isUUID from "validator/lib/isUUID";
|
||||
import { z } from "zod";
|
||||
import { SHARE_URL_SLUG_REGEX } from "@shared/utils/urlHelpers";
|
||||
import BaseSchema from "@server/routes/api/BaseSchema";
|
||||
|
||||
const DocumentsSortParamsSchema = z.object({
|
||||
/** Specifies the attributes by which documents will be sorted in the list */
|
||||
@@ -39,63 +40,70 @@ const BaseIdSchema = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
export const DocumentsListSchema = DocumentsSortParamsSchema.extend({
|
||||
/** Id of the user who created the doc */
|
||||
userId: z.string().uuid().optional(),
|
||||
export const DocumentsListSchema = BaseSchema.extend({
|
||||
body: DocumentsSortParamsSchema.extend({
|
||||
/** Id of the user who created the doc */
|
||||
userId: z.string().uuid().optional(),
|
||||
|
||||
/** Alias for userId - kept for backwards compatibility */
|
||||
user: z.string().uuid().optional(),
|
||||
/** Alias for userId - kept for backwards compatibility */
|
||||
user: z.string().uuid().optional(),
|
||||
|
||||
/** Id of the collection to which the document belongs */
|
||||
collectionId: z.string().uuid().optional(),
|
||||
/** Id of the collection to which the document belongs */
|
||||
collectionId: z.string().uuid().optional(),
|
||||
|
||||
/** Alias for collectionId - kept for backwards compatibility */
|
||||
collection: z.string().uuid().optional(),
|
||||
/** Alias for collectionId - kept for backwards compatibility */
|
||||
collection: z.string().uuid().optional(),
|
||||
|
||||
/** Id of the backlinked document */
|
||||
backlinkDocumentId: z.string().uuid().optional(),
|
||||
/** Id of the backlinked document */
|
||||
backlinkDocumentId: z.string().uuid().optional(),
|
||||
|
||||
/** Id of the parent document to which the document belongs */
|
||||
parentDocumentId: z.string().uuid().nullish(),
|
||||
/** Id of the parent document to which the document belongs */
|
||||
parentDocumentId: z.string().uuid().nullish(),
|
||||
|
||||
/** Boolean which denotes whether the document is a template */
|
||||
template: z.boolean().optional(),
|
||||
})
|
||||
/** Boolean which denotes whether the document is a template */
|
||||
template: z.boolean().optional(),
|
||||
}),
|
||||
// Maintains backwards compatibility
|
||||
.transform((doc) => {
|
||||
doc.collectionId = doc.collectionId || doc.collection;
|
||||
doc.userId = doc.userId || doc.user;
|
||||
delete doc.collection;
|
||||
delete doc.user;
|
||||
}).transform((req) => {
|
||||
req.body.collectionId = req.body.collectionId || req.body.collection;
|
||||
req.body.userId = req.body.userId || req.body.user;
|
||||
delete req.body.collection;
|
||||
delete req.body.user;
|
||||
|
||||
return doc;
|
||||
});
|
||||
return req;
|
||||
});
|
||||
|
||||
export type DocumentsListReq = z.infer<typeof DocumentsListSchema>;
|
||||
|
||||
export const DocumentsArchivedSchema = DocumentsSortParamsSchema.extend({});
|
||||
export const DocumentsArchivedSchema = BaseSchema.extend({
|
||||
body: DocumentsSortParamsSchema.extend({}),
|
||||
});
|
||||
|
||||
export type DocumentsArchivedReq = z.infer<typeof DocumentsArchivedSchema>;
|
||||
|
||||
export const DocumentsDeletedSchema = DocumentsSortParamsSchema.extend({});
|
||||
export const DocumentsDeletedSchema = BaseSchema.extend({
|
||||
body: DocumentsSortParamsSchema.extend({}),
|
||||
});
|
||||
|
||||
export type DocumentsDeletedReq = z.infer<typeof DocumentsDeletedSchema>;
|
||||
|
||||
export const DocumentsViewedSchema = DocumentsSortParamsSchema.extend({});
|
||||
export const DocumentsViewedSchema = BaseSchema.extend({
|
||||
body: DocumentsSortParamsSchema.extend({}),
|
||||
});
|
||||
|
||||
export type DocumentsViewedReq = z.infer<typeof DocumentsViewedSchema>;
|
||||
|
||||
export const DocumentsDraftsSchema = DocumentsSortParamsSchema.merge(
|
||||
DateFilterSchema
|
||||
).extend({
|
||||
/** Id of the collection to which the document belongs */
|
||||
collectionId: z.string().uuid().optional(),
|
||||
export const DocumentsDraftsSchema = BaseSchema.extend({
|
||||
body: DocumentsSortParamsSchema.merge(DateFilterSchema).extend({
|
||||
/** Id of the collection to which the document belongs */
|
||||
collectionId: z.string().uuid().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type DocumentsDraftsReq = z.infer<typeof DocumentsDraftsSchema>;
|
||||
|
||||
export const DocumentsInfoSchema = z
|
||||
.object({
|
||||
export const DocumentsInfoSchema = BaseSchema.extend({
|
||||
body: z.object({
|
||||
/** Id of the document to be retrieved */
|
||||
id: z.string().optional(),
|
||||
|
||||
@@ -107,136 +115,154 @@ export const DocumentsInfoSchema = z
|
||||
|
||||
/** Version of the API to be used */
|
||||
apiVersion: z.number().optional(),
|
||||
})
|
||||
.refine((obj) => !(isEmpty(obj.id) && isEmpty(obj.shareId)), {
|
||||
message: "one of id or shareId is required",
|
||||
});
|
||||
}),
|
||||
}).refine((req) => !(isEmpty(req.body.id) && isEmpty(req.body.shareId)), {
|
||||
message: "one of id or shareId is required",
|
||||
});
|
||||
|
||||
export type DocumentsInfoReq = z.infer<typeof DocumentsInfoSchema>;
|
||||
|
||||
export const DocumentsExportSchema = BaseIdSchema.extend({});
|
||||
export const DocumentsExportSchema = BaseSchema.extend({
|
||||
body: BaseIdSchema,
|
||||
});
|
||||
|
||||
export type DocumentsExportReq = z.infer<typeof DocumentsExportSchema>;
|
||||
|
||||
export const DocumentsRestoreSchema = BaseIdSchema.extend({
|
||||
/** Id of the collection to which the document belongs */
|
||||
collectionId: z.string().uuid().optional(),
|
||||
export const DocumentsRestoreSchema = BaseSchema.extend({
|
||||
body: BaseIdSchema.extend({
|
||||
/** Id of the collection to which the document belongs */
|
||||
collectionId: z.string().uuid().optional(),
|
||||
|
||||
/** Id of document revision */
|
||||
revisionId: z.string().uuid().optional(),
|
||||
/** Id of document revision */
|
||||
revisionId: z.string().uuid().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type DocumentsRestoreReq = z.infer<typeof DocumentsRestoreSchema>;
|
||||
|
||||
export const DocumentsSearchSchema = SearchQuerySchema.merge(
|
||||
DateFilterSchema
|
||||
).extend({
|
||||
/** Whether to include archived docs in results */
|
||||
includeArchived: z.boolean().optional(),
|
||||
export const DocumentsSearchSchema = BaseSchema.extend({
|
||||
body: SearchQuerySchema.merge(DateFilterSchema).extend({
|
||||
/** Whether to include archived docs in results */
|
||||
includeArchived: z.boolean().optional(),
|
||||
|
||||
/** Whether to include drafts in results */
|
||||
includeDrafts: z.boolean().optional(),
|
||||
/** Whether to include drafts in results */
|
||||
includeDrafts: z.boolean().optional(),
|
||||
|
||||
/** Filter results for team based on the collection */
|
||||
collectionId: z.string().uuid().optional(),
|
||||
/** Filter results for team based on the collection */
|
||||
collectionId: z.string().uuid().optional(),
|
||||
|
||||
/** Filter results based on user */
|
||||
userId: z.string().uuid().optional(),
|
||||
/** Filter results based on user */
|
||||
userId: z.string().uuid().optional(),
|
||||
|
||||
/** Filter results for the team derived from shareId */
|
||||
shareId: z
|
||||
.string()
|
||||
.refine((val) => isUUID(val) || SHARE_URL_SLUG_REGEX.test(val))
|
||||
.optional(),
|
||||
/** Filter results for the team derived from shareId */
|
||||
shareId: z
|
||||
.string()
|
||||
.refine((val) => isUUID(val) || SHARE_URL_SLUG_REGEX.test(val))
|
||||
.optional(),
|
||||
|
||||
/** Min words to be shown in the results snippets */
|
||||
snippetMinWords: z.number().default(20),
|
||||
/** Min words to be shown in the results snippets */
|
||||
snippetMinWords: z.number().default(20),
|
||||
|
||||
/** Max words to be accomodated in the results snippets */
|
||||
snippetMaxWords: z.number().default(30),
|
||||
/** Max words to be accomodated in the results snippets */
|
||||
snippetMaxWords: z.number().default(30),
|
||||
}),
|
||||
});
|
||||
|
||||
export type DocumentsSearchReq = z.infer<typeof DocumentsSearchSchema>;
|
||||
|
||||
export const DocumentsTemplatizeSchema = BaseIdSchema.extend({});
|
||||
export const DocumentsTemplatizeSchema = BaseSchema.extend({
|
||||
body: BaseIdSchema,
|
||||
});
|
||||
|
||||
export type DocumentsTemplatizeReq = z.infer<typeof DocumentsTemplatizeSchema>;
|
||||
|
||||
export const DocumentsUpdateSchema = BaseIdSchema.extend({
|
||||
/** Doc title to be updated */
|
||||
title: z.string().optional(),
|
||||
export const DocumentsUpdateSchema = BaseSchema.extend({
|
||||
body: BaseIdSchema.extend({
|
||||
/** Doc title to be updated */
|
||||
title: z.string().optional(),
|
||||
|
||||
/** Doc text to be updated */
|
||||
text: z.string().optional(),
|
||||
/** Doc text to be updated */
|
||||
text: z.string().optional(),
|
||||
|
||||
/** Boolean to denote if the doc should occupy full width */
|
||||
fullWidth: z.boolean().optional(),
|
||||
/** Boolean to denote if the doc should occupy full width */
|
||||
fullWidth: z.boolean().optional(),
|
||||
|
||||
/** Boolean to denote if the doc should be published */
|
||||
publish: z.boolean().optional(),
|
||||
/** Boolean to denote if the doc should be published */
|
||||
publish: z.boolean().optional(),
|
||||
|
||||
/** Revision to compare against document revision count */
|
||||
lastRevision: z.number().optional(),
|
||||
/** Revision to compare against document revision count */
|
||||
lastRevision: z.number().optional(),
|
||||
|
||||
/** Doc template Id */
|
||||
templateId: z.string().uuid().nullish(),
|
||||
/** Doc template Id */
|
||||
templateId: z.string().uuid().nullish(),
|
||||
|
||||
/** Doc collection Id */
|
||||
collectionId: z.string().uuid().optional(),
|
||||
/** Doc collection Id */
|
||||
collectionId: z.string().uuid().optional(),
|
||||
|
||||
/** Boolean to denote if text should be appended */
|
||||
append: z.boolean().optional(),
|
||||
}).refine((obj) => !(obj.append && !obj.text), {
|
||||
/** Boolean to denote if text should be appended */
|
||||
append: z.boolean().optional(),
|
||||
}),
|
||||
}).refine((req) => !(req.body.append && !req.body.text), {
|
||||
message: "text is required while appending",
|
||||
});
|
||||
|
||||
export type DocumentsUpdateReq = z.infer<typeof DocumentsUpdateSchema>;
|
||||
|
||||
export const DocumentsMoveSchema = BaseIdSchema.extend({
|
||||
/** Id of collection to which the doc is supposed to be moved */
|
||||
collectionId: z.string().uuid(),
|
||||
export const DocumentsMoveSchema = BaseSchema.extend({
|
||||
body: BaseIdSchema.extend({
|
||||
/** Id of collection to which the doc is supposed to be moved */
|
||||
collectionId: z.string().uuid(),
|
||||
|
||||
/** Parent Id, in case if the doc is moved to a new parent */
|
||||
parentDocumentId: z.string().uuid().nullish(),
|
||||
/** Parent Id, in case if the doc is moved to a new parent */
|
||||
parentDocumentId: z.string().uuid().nullish(),
|
||||
|
||||
/** Helps evaluate the new index in collection structure upon move */
|
||||
index: z.number().gte(0).optional(),
|
||||
}).refine((obj) => !(obj.parentDocumentId === obj.id), {
|
||||
/** Helps evaluate the new index in collection structure upon move */
|
||||
index: z.number().gte(0).optional(),
|
||||
}),
|
||||
}).refine((req) => !(req.body.parentDocumentId === req.body.id), {
|
||||
message: "infinite loop detected, cannot nest a document inside itself",
|
||||
});
|
||||
|
||||
export type DocumentsMoveReq = z.infer<typeof DocumentsMoveSchema>;
|
||||
|
||||
export const DocumentsArchiveSchema = BaseIdSchema.extend({});
|
||||
export const DocumentsArchiveSchema = BaseSchema.extend({
|
||||
body: BaseIdSchema,
|
||||
});
|
||||
|
||||
export type DocumentsArchiveReq = z.infer<typeof DocumentsArchiveSchema>;
|
||||
|
||||
export const DocumentsDeleteSchema = BaseIdSchema.extend({
|
||||
/** Whether to permanently delete the doc as opposed to soft-delete */
|
||||
permanent: z.boolean().optional(),
|
||||
export const DocumentsDeleteSchema = BaseSchema.extend({
|
||||
body: BaseIdSchema.extend({
|
||||
/** Whether to permanently delete the doc as opposed to soft-delete */
|
||||
permanent: z.boolean().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type DocumentsDeleteReq = z.infer<typeof DocumentsDeleteSchema>;
|
||||
|
||||
export const DocumentsUnpublishSchema = BaseIdSchema.extend({});
|
||||
export const DocumentsUnpublishSchema = BaseSchema.extend({
|
||||
body: BaseIdSchema,
|
||||
});
|
||||
|
||||
export type DocumentsUnpublishReq = z.infer<typeof DocumentsUnpublishSchema>;
|
||||
|
||||
export const DocumentsImportSchema = z.object({
|
||||
/** Whether to publish the imported docs. String due to multi-part form upload */
|
||||
publish: z.string().optional(),
|
||||
export const DocumentsImportSchema = BaseSchema.extend({
|
||||
body: z.object({
|
||||
/** Whether to publish the imported docs. String as this is always multipart/form-data */
|
||||
publish: z.preprocess((val) => val === "true", z.boolean()).optional(),
|
||||
|
||||
/** Import docs to this collection */
|
||||
collectionId: z.string().uuid(),
|
||||
/** Import docs to this collection */
|
||||
collectionId: z.string().uuid(),
|
||||
|
||||
/** Import under this parent doc */
|
||||
parentDocumentId: z.string().uuid().nullish(),
|
||||
/** Import under this parent doc */
|
||||
parentDocumentId: z.string().uuid().nullish(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type DocumentsImportReq = z.infer<typeof DocumentsImportSchema>;
|
||||
|
||||
export const DocumentsCreateSchema = z
|
||||
.object({
|
||||
export const DocumentsCreateSchema = BaseSchema.extend({
|
||||
body: z.object({
|
||||
/** Doc title */
|
||||
title: z.string().default(""),
|
||||
|
||||
@@ -257,14 +283,15 @@ export const DocumentsCreateSchema = z
|
||||
|
||||
/** Whether to create a template doc */
|
||||
template: z.boolean().optional(),
|
||||
})
|
||||
.refine((obj) => !(obj.parentDocumentId && !obj.collectionId), {
|
||||
}),
|
||||
})
|
||||
.refine((req) => !(req.body.parentDocumentId && !req.body.collectionId), {
|
||||
message: "collectionId is required to create a nested document",
|
||||
})
|
||||
.refine((obj) => !(obj.template && !obj.collectionId), {
|
||||
.refine((req) => !(req.body.template && !req.body.collectionId), {
|
||||
message: "collectionId is required to create a template document",
|
||||
})
|
||||
.refine((obj) => !(obj.publish && !obj.collectionId), {
|
||||
.refine((req) => !(req.body.publish && !req.body.collectionId), {
|
||||
message: "collectionId is required to publish",
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user