WIP: feature/zod-schemas #2225

Draft
dpschen wants to merge 2 commits from dpschen/frontend:feature/zod-schemas into main
45 changed files with 2779 additions and 1417 deletions

View File

@ -113,13 +113,15 @@
"sortablejs": "1.15.2",
"tippy.js": "6.3.7",
"ufo": "1.4.0",
"validator": "13.9.0",
"vue": "3.4.15",
"vue-advanced-cropper": "2.8.8",
"vue-flatpickr-component": "11.0.3",
"vue-i18n": "9.9.1",
"vue-router": "4.2.5",
"workbox-precaching": "7.0.0",
"zhyswan-vuedraggable": "4.1.3"
"zhyswan-vuedraggable": "4.1.3",
"zod": "3.20.6"
},
"devDependencies": {
"@4tw/cypress-drag-drop": "2.2.5",
@ -139,6 +141,7 @@
"@types/node": "20.11.10",
"@types/postcss-preset-env": "7.7.0",
"@types/sortablejs": "1.15.7",
"@types/validator": "13.7.12",
"@typescript-eslint/eslint-plugin": "6.20.0",
"@typescript-eslint/parser": "6.20.0",
"@vitejs/plugin-legacy": "5.3.0",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
import type {TypeOf} from 'zod'
import {IdSchema} from './common/id'
import {AbstractSchema} from './abstract'
export const LabelTaskSchema = AbstractSchema.extend({
id: IdSchema.nullable(),
taskId: IdSchema.nullable(),
labelId: IdSchema.nullable(),
})
export type LabelTask = TypeOf<typeof LabelTaskSchema>

View File

@ -0,0 +1,10 @@
import type {TypeOf} from 'zod'
import {object, nativeEnum} from 'zod'
import {RIGHTS} from '@/constants/rights'
export const AbstractSchema = object({
maxRight: nativeEnum(RIGHTS).nullable(),
})
export type IAbstract = TypeOf<typeof AbstractSchema>

View File

@ -0,0 +1,19 @@
import type {TypeOf} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {UserSchema} from './user'
import {FileSchema} from './file'
export const AttachmentSchema = AbstractSchema.extend({
id: IdSchema.default(0),
taskId: IdSchema.default(0), // iTaskSchema.shape.id
createdBy: UserSchema,
file: FileSchema,
created: DateSchema.nullable(),
})
export type IAttachment = TypeOf<typeof AttachmentSchema>

18
src/modelSchema/avatar.ts Normal file
View File

@ -0,0 +1,18 @@
import type {TypeOf} from 'zod'
import {z, string, object} from 'zod'
export const AVATAR_PROVIDER = [
'default',
'initials',
'gravatar',
'marble',
'upload',
] as const
export const AvatarProviderSchema = z.enum(AVATAR_PROVIDER)
export type IAvatarProvider = TypeOf<typeof AvatarProviderSchema>
export const AvatarSchema = object({
// FIXME: shouldn't the default be 'default'?
avatarProvider: string().or(AvatarProviderSchema).default(''),
})
export type IAvatar = TypeOf<typeof AvatarSchema>

View File

@ -0,0 +1,19 @@
import type {TypeOf} from 'zod'
import {object, record, string, unknown} from 'zod'
import {IdSchema} from './common/id'
export const BackgroundImageSchema = object({
id: IdSchema.default(0),
url: string().url().default(''),
thumb: string().default(''),
// FIXME: not sure if this needs to defined, since it seems provider specific
// {
// author: string(),
// authorName: string(),
// }
info: record(unknown()).default({}),
blurHash: string().default(''),
})
export type BackgroundImage = TypeOf<typeof BackgroundImageSchema>

26
src/modelSchema/bucket.ts Normal file
View File

@ -0,0 +1,26 @@
import type {TypeOf} from 'zod'
import {number, array, boolean} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {TextFieldSchema} from './common/textField'
import {AbstractSchema} from './abstract'
import {UserSchema} from './user'
import {TaskSchema} from './task'
export const BucketSchema = AbstractSchema.extend({
id: IdSchema.default(0),
title: TextFieldSchema,
listId: IdSchema.default(0),
limit: number().default(0),
tasks: array(TaskSchema).default([]),
isDoneBucket: boolean().default(false),
position: number().default(0),
createdBy: UserSchema.nullable(),
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
})
export type IBucket = TypeOf<typeof BucketSchema>

View File

@ -0,0 +1,14 @@
import type {TypeOf} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
export const CaldavTokenSchema = AbstractSchema.extend({
id: IdSchema,
created: DateSchema,
})
export type CaldavToken = TypeOf<typeof CaldavTokenSchema>

View File

@ -0,0 +1,22 @@
import type {TypeOf} from 'zod'
import {nativeEnum} from 'zod'
export const RELATION_KIND = {
'SUBTASK': 'subtask',
'PARENTTASK': 'parenttask',
'RELATED': 'related',
'DUPLICATES': 'duplicates',
'BLOCKING': 'blocking',
'BLOCKED': 'blocked',
'PROCEDES': 'precedes',
'FOLLOWS': 'follows',
'COPIEDFROM': 'copiedfrom',
'COPIEDTO': 'copiedto',
} as const
export const RELATION_KINDS = [...Object.values(RELATION_KIND)] as const
export const RelationKindSchema = nativeEnum(RELATION_KIND)
export type IRelationKind = TypeOf<typeof RelationKindSchema>

View File

@ -0,0 +1,11 @@
import {preprocess, date} from 'zod'
export const DateSchema = preprocess((arg) => {
if (
// FIXME: Add comment why we check for `0001`
typeof arg == 'string' && !arg.startsWith('0001') ||
arg instanceof Date
) {
return new Date(arg)
}
}, date())

View File

@ -0,0 +1,80 @@
import type {TypeOf} from 'zod'
import {z, nativeEnum, array, boolean, object, number} from 'zod'
export enum SORT_BY {
ID = 'id',
DONE = 'done',
TITLE = 'title',
PRIORITY = 'priority',
DONE_AT = 'done_at',
DUE_DATE = 'due_date',
START_DATE = 'start_date',
END_DATE = 'end_date',
PERCENT_DONE = 'percent_done',
CREATED = 'created',
UPDATED = 'updated',
POSITION = 'position',
KANBAN_POSITION = 'kanban_position',
}
export enum ORDER_BY {
ASC = 'asc',
DESC = 'desc',
NONE = 'none',
}
export enum FILTER_BY {
DONE = 'done',
DUE_DATE = 'due_date',
START_DATE = 'start_date',
END_DATE = 'end_date',
NAMESPACE = 'namespace',
ASSIGNEES = 'assignees',
LIST_ID = 'list_id',
BUCKET_ID = 'bucket_id',
PRIORITY = 'priority',
PERCENT_DONE = 'percent_done',
LABELS = 'labels',
UNDEFINED = 'undefined', // FIXME: Why do we have a value that is undefined as string?
}
export enum FILTER_COMPARATOR {
EQUALS = 'equals',
LESS = 'less',
GREATER = 'greater',
GREATER_EQUALS = 'greater_equals',
LESS_EQUALS = 'less_equals',
IN = 'in',
}
export enum FILTER_CONCAT {
AND = 'and',
OR = 'or',
IN = 'in',
}
const TASKS_PER_BUCKET = 25
export const FilterSchema = object({
sortBy: array(nativeEnum(SORT_BY)).default([SORT_BY.DONE, SORT_BY.ID]), // FIXME: create from taskSchema,
// fixme default order seem so also be `desc`
// see line from ListTable:
// if (typeof order === 'undefined' || order === 'none') {
orderBy: array(nativeEnum(ORDER_BY)).default([ORDER_BY.ASC, ORDER_BY.DESC]),
// FIXME: create from taskSchema
filterBy: array(nativeEnum(FILTER_BY)).default([FILTER_BY.DONE]),
// FIXME: create from taskSchema
// FIXME: might need to preprocess values, e.g. date.
// see line from 'filters.vue':
// params.filter_value = params.filter_value.map(v => v instanceof Date ? v.toISOString() : v)
filterValue: array(z.enum(['false'])).default(['false']),
// FIXME: is `in` value correct?
// found in `quick-actions.vue`:
// params.filter_comparator.push('in')
filterComparator: array(nativeEnum(FILTER_COMPARATOR)).default([FILTER_COMPARATOR.EQUALS]),
filterConcat: z.nativeEnum(FILTER_CONCAT).default(FILTER_CONCAT.AND),
filterIncludeNulls: boolean().default(true),
perPage: number().default(TASKS_PER_BUCKET), // FIXME: is perPage is just available for the bucket endpoint?
})
export type IFilter = TypeOf<typeof FilterSchema>

View File

@ -0,0 +1,10 @@
import {string} from 'zod'
import isHexColor from 'validator/lib/isHexColor'
export const HexColorSchema = string().transform(
(value) => {
if (!value || value.startsWith('#')) {
return value
}
return '#' + value
}).refine(value => isHexColor(value))

View File

@ -0,0 +1,6 @@
import {number, preprocess} from 'zod'
export const IdSchema = preprocess(
(value: unknown) => Number(value),
number().positive().int(),
)

View File

@ -1,24 +1,26 @@
import type {TypeOf} from 'zod'
import {nativeEnum, number, object, preprocess} from 'zod'
import {SECONDS_A_HOUR} from '@/constants/date'
import { REPEAT_TYPES, type IRepeatAfter } from '@/types/IRepeatAfter'
import { nativeEnum, number, object, preprocess } from 'zod'
import {REPEAT_TYPES, type IRepeatAfter} from '@/types/IRepeatAfter'
/**
* Parses `repeatAfterSeconds` into a usable js object.
*/
export function parseRepeatAfter(repeatAfterSeconds: number): IRepeatAfter {
let repeatAfter: IRepeatAfter = {type: 'hours', amount: repeatAfterSeconds / SECONDS_A_HOUR}
let repeatAfter: IRepeatAfter
// if its dividable by 24, its something with days, otherwise hours
// if its dividable by SECONDS_A_DAY, its something with days, otherwise hours
if (repeatAfterSeconds % SECONDS_A_DAY === 0) {
if (repeatAfterSeconds % SECONDS_A_WEEK === 0) {
repeatAfter = {type: 'weeks', amount: repeatAfterSeconds / SECONDS_A_WEEK}
} else if (repeatAfterSeconds % SECONDS_A_MONTH === 0) {
repeatAfter = {type:'months', amount: repeatAfterSeconds / SECONDS_A_MONTH}
} else if (repeatAfterSeconds % SECONDS_A_YEAR === 0) {
repeatAfter = {type: 'years', amount: repeatAfterSeconds / SECONDS_A_YEAR}
} else {
repeatAfter = {type: 'days', amount: repeatAfterSeconds / SECONDS_A_DAY}
}
repeatAfter = {type: REPEAT_TYPES.HOURS, amount: repeatAfterSeconds / SECONDS_A_HOUR}
} else if (repeatAfterSeconds % SECONDS_A_WEEK === 0) {
repeatAfter = {type: REPEAT_TYPES.WEEKS, amount: repeatAfterSeconds / SECONDS_A_WEEK}
} else if (repeatAfterSeconds % SECONDS_A_MONTH === 0) {
repeatAfter = {type: REPEAT_TYPES.MONTHS, amount: repeatAfterSeconds / SECONDS_A_MONTH}
} else if (repeatAfterSeconds % SECONDS_A_YEAR === 0) {
repeatAfter = {type: REPEAT_TYPES.YEARS, amount: repeatAfterSeconds / SECONDS_A_YEAR}
} else {
repeatAfter = {type: REPEAT_TYPES.DAYS, amount: repeatAfterSeconds / SECONDS_A_DAY}
}
return repeatAfter
}
@ -37,4 +39,6 @@ export const RepeatsSchema = preprocess(
type: nativeEnum(REPEAT_TYPES),
amount: number().int(),
}),
)
)
export type RepeatAfter = TypeOf<typeof RepeatsSchema>

View File

@ -0,0 +1,3 @@
import {string} from 'zod'
export const TextFieldSchema = string().transform((value) => value.trim()).default('')

View File

@ -0,0 +1,11 @@
import type {TypeOf} from 'zod'
import {string} from 'zod'
import {AbstractSchema} from './abstract'
export const EmailUpdateSchema = AbstractSchema.extend({
newEmail: string().email().default(''),
password: string().default(''),
})
export type EmailUpdate = TypeOf<typeof EmailUpdateSchema>

15
src/modelSchema/file.ts Normal file
View File

@ -0,0 +1,15 @@
import type {TypeOf} from 'zod'
import {object, number, string} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
export const FileSchema = object({
id: IdSchema.default(0),
mime: string().default(''),
name: string().default(''),
size: number().default(0),
created: DateSchema.nullable(),
})
export type File = TypeOf<typeof FileSchema>

33
src/modelSchema/label.ts Normal file
View File

@ -0,0 +1,33 @@
import type {TypeOf} from 'zod'
import {string} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {HexColorSchema} from './common/hexColor'
import {UserSchema} from './user'
import {AbstractSchema} from './abstract'
import {colorIsDark} from '@/helpers/color/colorIsDark'
const DEFAULT_LABEL_BACKGROUND_COLOR = 'e8e8e8'
export const LabelSchema = AbstractSchema.extend({
id: IdSchema.default(0),
title: string().default(''),
hexColor: HexColorSchema.default(DEFAULT_LABEL_BACKGROUND_COLOR),
textColor: string(), // implicit
description: string().default(''),
createdBy: UserSchema, // FIXME: default: current user?
listId: IdSchema.default(0),
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
}).transform((obj) => {
// FIXME: remove textColor location => should be defined in UI
obj.textColor = colorIsDark(obj.hexColor) ? '#4a4a4a' : '#ffffff'
return obj
},
)
export type ILabel = TypeOf<typeof LabelSchema>

View File

@ -0,0 +1,26 @@
import type {TypeOf} from 'zod'
import {number, string, nativeEnum} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {UserSchema} from './user'
import {RIGHTS} from '@/constants/rights'
export const LinkShareSchema = AbstractSchema.extend({
id: IdSchema.default(0),
hash: string().default(''),
right: nativeEnum(RIGHTS).default(RIGHTS.READ),
sharedBy: UserSchema,
sharingType: number().default(0), // FIXME: use correct numbers
listId: IdSchema.default(0),
name: string().default(''),
password: string().default(''),
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
})
export type LinkShare = TypeOf<typeof LinkShareSchema>

32
src/modelSchema/list.ts Normal file
View File

@ -0,0 +1,32 @@
import type {TypeOf} from 'zod'
import {boolean, number, string, array, any} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {SubscriptionSchema} from './subscription'
import {TaskSchema} from './task'
import {UserSchema} from './user'
export const ListSchema = AbstractSchema.extend({
id: IdSchema.default(0),
hash: string().default(''),
description: string().default(''),
owner: UserSchema,
tasks: array(TaskSchema),
namespaceId: IdSchema.default(0), // INamespace['id'],
isArchived: boolean().default(false),
hexColor: string().default(''),
identifier: string().default(''),
backgroundInformation: any().nullable().default(null), // FIXME: what is this for?
isFavorite: boolean().default(false),
subscription: SubscriptionSchema.nullable(),
position: number().default(0),
backgroundBlurHash: string().default(''),
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
})
export type List = TypeOf<typeof ListSchema>

View File

@ -0,0 +1,14 @@
import type {TypeOf} from 'zod'
import {IdSchema} from './common/id'
import {AbstractSchema} from './abstract'
import {ListSchema} from './list'
export const ListDuplicationSchema = AbstractSchema.extend({
listId: IdSchema.default(0),
namespaceId: IdSchema.default(0), // INamespace['id'],
list: ListSchema,
})
export type ListDuplication = TypeOf<typeof ListDuplicationSchema>

View File

@ -0,0 +1,26 @@
import type {TypeOf} from 'zod'
import {boolean, string, array} from 'zod'
import {IdSchema} from './common/id'
import {HexColorSchema} from './common/hexColor'
import {AbstractSchema} from './abstract'
import {ListSchema} from './list'
import {UserSchema} from './user'
import {SubscriptionSchema} from './subscription'
export const NamespaceSchema = AbstractSchema.extend({
id: IdSchema.default(0),
title: string().default(''),
description: string().default(''),
owner: UserSchema,
lists: array(ListSchema),
isArchived: boolean().default(false),
hexColor: HexColorSchema.default(''),
subscription: SubscriptionSchema.nullable(),
created: IdSchema.nullable(),
updated: IdSchema.nullable(),
})
export type Namespace = TypeOf<typeof NamespaceSchema>

View File

@ -0,0 +1,54 @@
import type {TypeOf} from 'zod'
import {union, boolean, object, string} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {TaskSchema} from './task'
import {TaskCommentSchema} from './taskComment'
import {TeamSchema} from './team'
import {UserSchema} from './user'
const NotificationTypeSchema = object({
doer: UserSchema,
})
const NotificationTypeTask = NotificationTypeSchema.extend({
task: TaskSchema,
comment: TaskCommentSchema,
})
const NotificationTypeAssigned = NotificationTypeSchema.extend({
task: TaskSchema,
assignee: UserSchema,
})
const NotificationTypeDeleted = NotificationTypeSchema.extend({
task: TaskSchema,
})
const NotificationTypeCreated = NotificationTypeSchema.extend({
task: TaskSchema,
})
const NotificationTypeMemberAdded = NotificationTypeSchema.extend({
member: UserSchema,
team: TeamSchema,
})
export const NotificationSchema = AbstractSchema.extend({
id: IdSchema.default(0),
name: string().default(''),
notification: union([
NotificationTypeTask,
NotificationTypeAssigned,
NotificationTypeDeleted,
NotificationTypeCreated,
NotificationTypeMemberAdded,
]),
read: boolean().default(false),
readAt: DateSchema.nullable(),
})
export type Notification = TypeOf<typeof NotificationSchema>

View File

@ -0,0 +1,13 @@
import type {TypeOf} from 'zod'
import {string} from 'zod'
import {AbstractSchema} from './abstract'
// FIXME: is it correct that this extends the Abstract Schema?
export const PasswordResetSchema = AbstractSchema.extend({
token: string().default(''),
newPassword: string().default(''),
email: string().email().default(''),
})
export type PasswordReset = TypeOf<typeof PasswordResetSchema>

View File

@ -0,0 +1,15 @@
import type {TypeOf} from 'zod'
import {string} from 'zod'
import {AbstractSchema} from './abstract'
// FIXME: is it correct that this extends the Abstract Schema?
export const PasswordUpdateSchema = AbstractSchema.extend({
newPassword: string().default(''),
oldPassword: string().default(''),
}).refine((data) => data.newPassword === data.oldPassword, {
message: 'Passwords don\'t match',
path: ['confirm'], // path of error
})
export type PasswordUpdate = TypeOf<typeof PasswordUpdateSchema>

View File

@ -0,0 +1,23 @@
import type {TypeOf} from 'zod'
import {string} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {FilterSchema} from './common/filter'
import {AbstractSchema} from './abstract'
import {UserSchema} from './user'
// FIXME: is it correct that this extends the Abstract Schema?
export const SavedFilterSchema = AbstractSchema.extend({
id: IdSchema.default(0),
title: string().default(''),
description: string().default(''),
filters: FilterSchema,
owner: UserSchema,
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
})
export type SavedFilter = TypeOf<typeof SavedFilterSchema>

View File

@ -0,0 +1,19 @@
import type {TypeOf} from 'zod'
import {string} from 'zod'
import {DateSchema} from './common/date'
import {IdSchema} from './common/id'
import {AbstractSchema} from './abstract'
import {UserSchema} from './user'
export const SubscriptionSchema = AbstractSchema.extend({
id: IdSchema.default(0),
entity: string().default(''), // FIXME: correct type?
entityId: IdSchema.default(0), // FIXME: correct type?
user: UserSchema,
created: DateSchema.nullable(),
})
export type Subscription = TypeOf<typeof SubscriptionSchema>

119
src/modelSchema/task.ts Normal file
View File

@ -0,0 +1,119 @@
import type {ZodType, TypeOf} from 'zod'
import {nativeEnum, boolean, number, string, array, record, unknown, lazy} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {HexColorSchema} from './common/hexColor'
import {TextFieldSchema} from './common/textField'
import {RelationKindSchema} from './common/RelationKind'
import {RepeatsSchema} from './common/repeats'
import {AbstractSchema} from './abstract'
import {AttachmentSchema} from './attachment'
import {LabelSchema} from './label'
import {SubscriptionSchema} from './subscription'
import {UserSchema} from './user'
import {PRIORITIES} from '@/constants/priorities'
import {TASK_REPEAT_MODES} from '@/types/IRepeatMode'
const LabelsSchema = array(LabelSchema)
.transform((labels) => labels.sort((f, s) => f.title > s.title ? 1 : -1)) // FIXME: use
.default([])
export type ILabels = TypeOf<typeof LabelsSchema>
const RelatedTasksSchema = record(RelationKindSchema, record(string(), unknown()))
export type IRelatedTasksSchema = TypeOf<typeof RelatedTasksSchema>
const RelatedTasksLazySchema : ZodType<Task['relatedTasks']> = lazy(() =>
record(RelationKindSchema, TaskSchema),
)
export type IRelatedTasksLazySchema = TypeOf<typeof RelatedTasksLazySchema>
// export interface ITask extends IAbstract {
// id: number
// title: string
// description: string
// done: boolean
// doneAt: Date | null
// priority: Priority
// labels: ILabel[]
// assignees: IUser[]
// dueDate: Date | null
// startDate: Date | null
// endDate: Date | null
// repeatAfter: number | IRepeatAfter
// repeatFromCurrentDate: boolean
// repeatMode: IRepeatMode
// reminderDates: Date[]
// parentTaskId: ITask['id']
// hexColor: string
// percentDone: number
// relatedTasks: Partial<Record<IRelationKind, ITask>>,
// attachments: IAttachment[]
// identifier: string
// index: number
// isFavorite: boolean
// subscription: ISubscription
// position: number
// kanbanPosition: number
// createdBy: IUser
// created: Date
// updated: Date
// listId: IList['id'] // Meta, only used when creating a new task
// bucketId: IBucket['id']
// }
export const TaskSchema = AbstractSchema.extend({
id: IdSchema.default(0),
title: TextFieldSchema,
description: TextFieldSchema,
done: boolean().default(false),
doneAt: DateSchema.nullable().default(null),
priority: nativeEnum(PRIORITIES).default(PRIORITIES.UNSET),
labels: LabelsSchema,
assignees: array(UserSchema).default([]),
dueDate: DateSchema.nullable(), // FIXME: default value is `0`. Shouldn't this be `null`?
startDate: DateSchema.nullable(), // FIXME: default value is `0`. Shouldn't this be `null`?
endDate: DateSchema.nullable(), // FIXME: default value is `0`. Shouldn't this be `null`?
repeatAfter: RepeatsSchema, // FIXME: default value is `0`. Shouldn't this be `null`?
repeatFromCurrentDate: boolean().default(false),
repeatMode: nativeEnum(TASK_REPEAT_MODES).default(TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT),
// TODO: schedule notifications
// FIXME: triggered notificaitons not supported anymore / remove feature?
reminderDates: array(DateSchema).default([]),
parentTaskId: IdSchema.default(0), // shouldn't this have `null` as default?
hexColor: HexColorSchema.default(''),
percentDone: number().default(0),
relatedTasks: RelatedTasksSchema.default({}),
attachments: array(AttachmentSchema).default([]),
identifier: string().default(''),
index: number().default(0),
isFavorite: boolean().default(false),
subscription: SubscriptionSchema.nullable().default(null),
position: number().default(0),
kanbanPosition: number().default(0),
createdBy: UserSchema,
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
listId: IdSchema.default(0), //IList['id'], // Meta, only used when creating a new task
bucketId: IdSchema.default(0), // IBucket['id'],
}).transform((obj) => {
if (obj.identifier === `-${obj.index}`) {
obj.identifier = ''
}
return obj
})
export type Task = TypeOf<typeof TaskSchema>

View File

@ -0,0 +1,14 @@
import type {TypeOf} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
export const TaskAssigneeSchema = AbstractSchema.extend({
created: DateSchema.nullable(),
userId: IdSchema.default(0), // IUser['id']
taskId: IdSchema.default(0), // ITask['id']
})
export type TaskAssignee = TypeOf<typeof TaskAssigneeSchema>

View File

@ -0,0 +1,20 @@
import type {TypeOf} from 'zod'
import {string} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {UserSchema} from './user'
export const TaskCommentSchema = AbstractSchema.extend({
id: IdSchema.default(0),
taskId: IdSchema.default(0),
comment: string().default(''),
author: UserSchema,
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
})
export type TaskComment = TypeOf<typeof TaskCommentSchema>

View File

@ -0,0 +1,24 @@
import type {TypeOf} from 'zod'
import {nativeEnum} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {UserSchema} from './user'
import {RELATION_KIND} from '@/types/IRelationKind'
export const TaskRelationSchema = AbstractSchema.extend({
id: IdSchema.default(0),
otherTaskId: IdSchema.default(0),
taskId: IdSchema.default(0),
relationKind: nativeEnum(RELATION_KIND).nullable().default(null), // FIXME: default value was empty string?
createdBy: UserSchema,
// FIXME: shouldn't the empty value of dates be `new Date()`
// Because e.g. : `new Date(null)` => Thu Jan 01 1970 01:00:00 GMT+0100 (Central European Standard Time)
created: DateSchema.nullable().default(null),
})
export type ITaskRelation = TypeOf<typeof TaskRelationSchema>

25
src/modelSchema/team.ts Normal file
View File

@ -0,0 +1,25 @@
import type {TypeOf} from 'zod'
import {array, nativeEnum, string} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {UserSchema} from './user'
import {TeamMemberSchema} from './teamMember'
import {RIGHTS} from '@/constants/rights'
export const TeamSchema = AbstractSchema.extend({
id: IdSchema.default(0),
name: string().default(''),
description: string().default(''),
members: array(TeamMemberSchema),
right: nativeEnum(RIGHTS).default(RIGHTS.READ),
createdBy: UserSchema, // FIXME: default was {},
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
})
export type Team = TypeOf<typeof TeamSchema>

View File

@ -0,0 +1,11 @@
import type {TypeOf} from 'zod'
import {IdSchema} from './common/id'
import {TeamShareBaseSchema} from './teamShareBase'
export const TeamListSchema = TeamShareBaseSchema.extend({
listId: IdSchema.default(0), // IList['id']
})
export type TeamList = TypeOf<typeof TeamListSchema>

View File

@ -0,0 +1,13 @@
import type {TypeOf} from 'zod'
import {boolean} from 'zod'
import {IdSchema} from './common/id'
import {UserSchema} from './user'
export const TeamMemberSchema = UserSchema.extend({
admin: boolean().default(false),
teamId: IdSchema.default(0), // IList['id']
})
export type TeamMember = TypeOf<typeof TeamMemberSchema>

View File

@ -0,0 +1,11 @@
import type {TypeOf} from 'zod'
import {IdSchema} from './common/id'
import {TeamShareBaseSchema} from './teamShareBase'
export const TeamNamespaceSchema = TeamShareBaseSchema.extend({
namespaceId: IdSchema.default(0), // INamespace['id']
})
export type ITeamNamespace = TypeOf<typeof TeamNamespaceSchema>

View File

@ -0,0 +1,19 @@
import type {TypeOf} from 'zod'
import {nativeEnum} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {RIGHTS} from '@/constants/rights'
export const TeamShareBaseSchema = AbstractSchema.extend({
teamId: IdSchema.default(0), // ITeam['id']
right: nativeEnum(RIGHTS).default(RIGHTS.READ),
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
})
export type ITeamShareBase = TypeOf<typeof TeamShareBaseSchema>

9
src/modelSchema/token.ts Normal file
View File

@ -0,0 +1,9 @@
import type {TypeOf} from 'zod'
import {object, string} from 'zod'
export const TokenSchema = object({
token: string(),
})
export type IToken = TypeOf<typeof TokenSchema>

12
src/modelSchema/totp.ts Normal file
View File

@ -0,0 +1,12 @@
import type {TypeOf} from 'zod'
import {string, boolean} from 'zod'
import {AbstractSchema} from './abstract'
export const TotpSchema = AbstractSchema.extend({
secret: string().default(''),
enabled: boolean().default(false),
url: string().url().default(''),
})
export type Totp = TypeOf<typeof TotpSchema>

21
src/modelSchema/user.ts Normal file
View File

@ -0,0 +1,21 @@
import type {TypeOf} from 'zod'
import {string} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {UserSettingsSchema} from './userSettings'
export const UserSchema = AbstractSchema.extend({
id: IdSchema.default(0),
email: string().email().default(''),
username: string().default(''),
name: string().default(''),
settings: UserSettingsSchema.nullable(),
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
})
export type User = TypeOf<typeof UserSchema>

View File

@ -0,0 +1,11 @@
import type {TypeOf} from 'zod'
import {IdSchema} from './common/id'
import {UserShareBaseSchema} from './userShareBase'
export const UserListSchema = UserShareBaseSchema.extend({
listId: IdSchema.default(0), // IList['id']
})
export type IUserList = TypeOf<typeof UserListSchema>

View File

@ -0,0 +1,11 @@
import type {TypeOf} from 'zod'
import {IdSchema} from './common/id'
import {UserShareBaseSchema} from './userShareBase'
export const UserNamespaceSchema = UserShareBaseSchema.extend({
namespaceId: IdSchema.default(0), // INamespace['id']
})
export type IUserNamespace = TypeOf<typeof UserNamespaceSchema>

View File

@ -0,0 +1,29 @@
import type {TypeOf} from 'zod'
import {boolean, string, undefined, nativeEnum} from 'zod'
import {IdSchema} from './common/id'
import {AbstractSchema} from './abstract'
const WEEKDAYS = {
MONDAY: 0,
TUESDAY: 1,
WEDNESDAY: 2,
THURSDAY: 3,
FRIDAY: 4,
SATURDAY: 5,
SUNDAY: 6,
} as const
export const UserSettingsSchema = AbstractSchema.extend({
name: string().default(''),
emailRemindersEnabled: boolean().default(true),
discoverableByName: boolean().default(false),
discoverableByEmail: boolean().default(false),
overdueTasksRemindersEnabled: boolean().default(true),
defaultListId: IdSchema.or(undefined()), // iListSchema['id'] // FIXME: shouldn't this be `null`?
weekStart: nativeEnum(WEEKDAYS).default(WEEKDAYS.MONDAY),
timezone: string().default(''),
})
export type IUserSettings = TypeOf<typeof UserSettingsSchema>

View File

@ -0,0 +1,19 @@
import type {TypeOf} from 'zod'
import {nativeEnum} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {RIGHTS} from '@/constants/rights'
export const UserShareBaseSchema = AbstractSchema.extend({
userId: IdSchema, // FIXME: default of model is `''`
right: nativeEnum(RIGHTS).default(RIGHTS.READ),
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
})
export type TeamMember = TypeOf<typeof UserShareBaseSchema>

View File

@ -1,6 +1,13 @@
import type {IAbstract} from './IAbstract'
export type AvatarProvider = 'default' | 'initials' | 'gravatar' | 'marble' | 'upload'
export const AVATAR_PROVIDER = [
'default',
'initials',
'gravatar',
'marble',
'upload',
] as const
export type AvatarProvider = typeof AVATAR_PROVIDER[number]
export interface IAvatar extends IAbstract {
avatarProvider: AvatarProvider