mirror of
https://github.com/colanode/colanode.git
synced 2025-12-29 00:25:03 +01:00
Add a core package to be reused by desktop and server
This commit is contained in:
@@ -22,5 +22,10 @@
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "^5.6.3",
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"diff": "^7.0.0",
|
||||
"yjs": "^13.6.20",
|
||||
"zod": "^3.23.8"
|
||||
}
|
||||
}
|
||||
|
||||
313
packages/core/src/crdt.ts
Normal file
313
packages/core/src/crdt.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import { z } from 'zod';
|
||||
import * as Y from 'yjs';
|
||||
import { ZodText } from './registry/zod';
|
||||
import { isEqual } from 'lodash';
|
||||
import { diffChars } from 'diff';
|
||||
|
||||
export const applyCrdt = (
|
||||
schema: z.ZodSchema,
|
||||
attributes: z.infer<typeof schema>,
|
||||
attributesMap: Y.Map<any>
|
||||
) => {
|
||||
if (!(schema instanceof z.ZodObject)) {
|
||||
throw new Error('Schema must be a ZodObject');
|
||||
}
|
||||
|
||||
applyObjectChanges(schema, attributes, attributesMap);
|
||||
|
||||
const parseResult = schema.safeParse(attributesMap.toJSON());
|
||||
if (!parseResult.success) {
|
||||
throw new Error('Invalid attributes', parseResult.error);
|
||||
}
|
||||
};
|
||||
|
||||
const applyObjectChanges = (
|
||||
schema: z.ZodObject<any, any, any, any>,
|
||||
attributes: any,
|
||||
yMap: Y.Map<any>
|
||||
) => {
|
||||
for (const [key, value] of Object.entries(attributes)) {
|
||||
if (value === null) {
|
||||
yMap.set(key, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
const schemaField = extractType(schema.shape[key], value);
|
||||
if (schemaField instanceof z.ZodObject) {
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error('Value must be an object');
|
||||
}
|
||||
|
||||
let nestedMap = yMap.get(key);
|
||||
if (!(nestedMap instanceof Y.Map)) {
|
||||
nestedMap = new Y.Map();
|
||||
yMap.set(key, nestedMap);
|
||||
}
|
||||
|
||||
applyObjectChanges(schemaField, value, nestedMap);
|
||||
} else if (schemaField instanceof z.ZodRecord) {
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error('Value must be an object');
|
||||
}
|
||||
|
||||
let nestedMap = yMap.get(key);
|
||||
if (!(nestedMap instanceof Y.Map)) {
|
||||
nestedMap = new Y.Map();
|
||||
yMap.set(key, nestedMap);
|
||||
}
|
||||
|
||||
applyRecordChanges(schemaField, value, nestedMap);
|
||||
} else if (schemaField instanceof z.ZodArray) {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error('Value must be an array');
|
||||
}
|
||||
|
||||
let yArray = yMap.get(key);
|
||||
if (!(yArray instanceof Y.Array)) {
|
||||
yArray = new Y.Array();
|
||||
yMap.set(key, yArray);
|
||||
}
|
||||
|
||||
applyArrayChanges(schemaField, value, yArray);
|
||||
} else if (schemaField instanceof ZodText) {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('Value must be a string');
|
||||
}
|
||||
|
||||
let yText = yMap.get(key);
|
||||
if (!(yText instanceof Y.Text)) {
|
||||
yText = new Y.Text();
|
||||
yMap.set(key, yText);
|
||||
}
|
||||
|
||||
applyTextChanges(value, yText);
|
||||
} else {
|
||||
const currentValue = yMap.get(key);
|
||||
|
||||
if (!isEqual(currentValue, value)) {
|
||||
yMap.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const deletedKeys = Array.from(yMap.keys()).filter(
|
||||
(key) => !attributes.hasOwnProperty(key)
|
||||
);
|
||||
|
||||
for (const key of deletedKeys) {
|
||||
yMap.delete(key);
|
||||
}
|
||||
};
|
||||
|
||||
const applyArrayChanges = (
|
||||
schemaField: z.ZodArray<any>,
|
||||
value: Array<any>,
|
||||
yArray: Y.Array<any>
|
||||
) => {
|
||||
const itemSchema = extractType(schemaField.element, value);
|
||||
const length = value.length;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const item = value[i];
|
||||
|
||||
if (item === null) {
|
||||
yArray.delete(i, 1);
|
||||
yArray.insert(i, [null]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (itemSchema instanceof z.ZodObject) {
|
||||
if (yArray.length <= i) {
|
||||
const nestedMap = new Y.Map();
|
||||
yArray.insert(i, [nestedMap]);
|
||||
}
|
||||
|
||||
let nestedMap = yArray.get(i);
|
||||
if (!(nestedMap instanceof Y.Map)) {
|
||||
nestedMap = new Y.Map();
|
||||
yArray.delete(i, 1);
|
||||
yArray.insert(i, [nestedMap]);
|
||||
}
|
||||
|
||||
applyObjectChanges(itemSchema, item, nestedMap);
|
||||
} else if (itemSchema instanceof z.ZodRecord) {
|
||||
if (yArray.length <= i) {
|
||||
const nestedMap = new Y.Map();
|
||||
yArray.insert(i, [nestedMap]);
|
||||
}
|
||||
|
||||
let nestedMap = yArray.get(i);
|
||||
if (!(nestedMap instanceof Y.Map)) {
|
||||
nestedMap = new Y.Map();
|
||||
yArray.delete(i, 1);
|
||||
yArray.insert(i, [nestedMap]);
|
||||
}
|
||||
|
||||
applyRecordChanges(itemSchema, item, nestedMap);
|
||||
} else if (itemSchema instanceof z.ZodRecord) {
|
||||
if (yArray.length <= i) {
|
||||
const nestedMap = new Y.Map();
|
||||
yArray.insert(i, [nestedMap]);
|
||||
}
|
||||
|
||||
let nestedMap = yArray.get(i);
|
||||
if (!(nestedMap instanceof Y.Map)) {
|
||||
nestedMap = new Y.Map();
|
||||
yArray.delete(i, 1);
|
||||
yArray.insert(i, [nestedMap]);
|
||||
}
|
||||
|
||||
applyRecordChanges(itemSchema, item, nestedMap);
|
||||
} else if (itemSchema instanceof ZodText) {
|
||||
if (yArray.length <= i) {
|
||||
const yText = new Y.Text();
|
||||
yArray.insert(i, [yText]);
|
||||
}
|
||||
|
||||
let yText = yArray.get(i);
|
||||
if (!(yText instanceof Y.Text)) {
|
||||
yText = new Y.Text();
|
||||
yArray.delete(i, 1);
|
||||
yArray.insert(i, [yText]);
|
||||
}
|
||||
|
||||
applyTextChanges(item, yText);
|
||||
} else {
|
||||
if (yArray.length <= i) {
|
||||
yArray.insert(i, [item]);
|
||||
} else {
|
||||
const currentItem = yArray.get(i);
|
||||
if (!isEqual(currentItem, item)) {
|
||||
yArray.delete(i);
|
||||
yArray.insert(i, [item]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (yArray.length > length) {
|
||||
yArray.delete(yArray.length, yArray.length - length);
|
||||
}
|
||||
};
|
||||
|
||||
const applyRecordChanges = (
|
||||
schemaField: z.ZodRecord<any, any>,
|
||||
record: Record<any, any>,
|
||||
yMap: Y.Map<any>
|
||||
) => {
|
||||
const valueSchema = extractType(schemaField.valueSchema, record);
|
||||
for (const [key, value] of Object.entries(record)) {
|
||||
if (value === null) {
|
||||
yMap.set(key, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (valueSchema instanceof z.ZodObject) {
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error('Value must be an object');
|
||||
}
|
||||
|
||||
let nestedMap = yMap.get(key);
|
||||
if (!(nestedMap instanceof Y.Map)) {
|
||||
nestedMap = new Y.Map();
|
||||
yMap.set(key, nestedMap);
|
||||
}
|
||||
|
||||
applyObjectChanges(valueSchema, value, nestedMap);
|
||||
} else if (valueSchema instanceof z.ZodRecord) {
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error('Value must be an object');
|
||||
}
|
||||
|
||||
let nestedMap = yMap.get(key);
|
||||
if (!(nestedMap instanceof Y.Map)) {
|
||||
nestedMap = new Y.Map();
|
||||
yMap.set(key, nestedMap);
|
||||
}
|
||||
|
||||
applyRecordChanges(valueSchema, value, nestedMap);
|
||||
} else if (valueSchema instanceof z.ZodArray) {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error('Value must be an array');
|
||||
}
|
||||
|
||||
let yArray = yMap.get(key);
|
||||
if (!(yArray instanceof Y.Array)) {
|
||||
yArray = new Y.Array();
|
||||
yMap.set(key, yArray);
|
||||
}
|
||||
|
||||
applyArrayChanges(valueSchema, value, yArray);
|
||||
} else if (valueSchema instanceof ZodText) {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('Value must be a string');
|
||||
}
|
||||
|
||||
let yText = yMap.get(key);
|
||||
if (!(yText instanceof Y.Text)) {
|
||||
yText = new Y.Text();
|
||||
yMap.set(key, yText);
|
||||
}
|
||||
|
||||
applyTextChanges(value, yText);
|
||||
} else {
|
||||
const currentValue = yMap.get(key);
|
||||
if (!isEqual(currentValue, value)) {
|
||||
yMap.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const deletedKeys = Array.from(yMap.keys()).filter(
|
||||
(key) => !record.hasOwnProperty(key)
|
||||
);
|
||||
|
||||
for (const key of deletedKeys) {
|
||||
yMap.delete(key);
|
||||
}
|
||||
};
|
||||
|
||||
const applyTextChanges = (value: string, yText: Y.Text) => {
|
||||
const currentText = yText.toString();
|
||||
const newText = value ? value.toString() : '';
|
||||
|
||||
if (!isEqual(currentText, newText)) {
|
||||
const diffs = diffChars(currentText, newText);
|
||||
let index = 0;
|
||||
|
||||
for (const diff of diffs) {
|
||||
if (diff.added) {
|
||||
yText.insert(index, diff.value);
|
||||
index += diff.value.length;
|
||||
} else if (diff.removed) {
|
||||
yText.delete(index, diff.value.length);
|
||||
index -= diff.value.length;
|
||||
} else {
|
||||
index += diff.value.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const extractType = (
|
||||
schema: z.ZodType<any, any, any>,
|
||||
value: any
|
||||
): z.ZodType<any, any, any> => {
|
||||
if (schema instanceof z.ZodOptional) {
|
||||
return extractType(schema.unwrap(), value);
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodNullable) {
|
||||
return extractType(schema.unwrap(), value);
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodUnion) {
|
||||
for (const option of schema.options) {
|
||||
if (option.safeParse(value).success) {
|
||||
return extractType(option, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return schema;
|
||||
};
|
||||
@@ -1,3 +1,303 @@
|
||||
export const core = () => {
|
||||
return 'core library';
|
||||
import * as yjs from 'yjs';
|
||||
import { z as zod } from 'zod';
|
||||
import { applyCrdt } from './crdt';
|
||||
import {
|
||||
ChannelAttributes,
|
||||
channelAttributesSchema,
|
||||
channelModel,
|
||||
} from './registry/channel';
|
||||
import {
|
||||
PageAttributes,
|
||||
pageAttributesSchema,
|
||||
pageModel,
|
||||
} from './registry/page';
|
||||
import {
|
||||
ChatAttributes,
|
||||
chatAttributesSchema,
|
||||
chatModel,
|
||||
} from './registry/chat';
|
||||
import {
|
||||
SpaceAttributes,
|
||||
spaceAttributesSchema,
|
||||
spaceModel,
|
||||
} from './registry/space';
|
||||
import {
|
||||
UserAttributes,
|
||||
userAttributesSchema,
|
||||
userModel,
|
||||
} from './registry/user';
|
||||
import {
|
||||
MessageAttributes,
|
||||
messageAttributesSchema,
|
||||
messageModel,
|
||||
} from './registry/message';
|
||||
import {
|
||||
ViewFieldAttributes,
|
||||
viewFieldAttributesSchema,
|
||||
ViewFieldFilterAttributes,
|
||||
viewFieldFilterAttributesSchema,
|
||||
ViewGroupFilterAttributes,
|
||||
viewGroupFilterAttributesSchema,
|
||||
ViewFilterAttributes,
|
||||
viewFilterAttributesSchema,
|
||||
ViewSortAttributes,
|
||||
viewSortAttributesSchema,
|
||||
ViewAttributes,
|
||||
viewAttributesSchema,
|
||||
DatabaseAttributes,
|
||||
databaseAttributesSchema,
|
||||
databaseModel,
|
||||
} from './registry/database';
|
||||
import {
|
||||
FileAttributes,
|
||||
fileAttributesSchema,
|
||||
fileModel,
|
||||
} from './registry/file';
|
||||
import {
|
||||
FolderAttributes,
|
||||
folderAttributesSchema,
|
||||
folderModel,
|
||||
} from './registry/folder';
|
||||
import {
|
||||
RecordAttributes,
|
||||
recordAttributesSchema,
|
||||
recordModel,
|
||||
} from './registry/record';
|
||||
import {
|
||||
blockLeafSchema,
|
||||
blockSchema,
|
||||
BlockLeaf,
|
||||
Block,
|
||||
} from './registry/block';
|
||||
import {
|
||||
selectOptionAttributesSchema,
|
||||
booleanFieldAttributesSchema,
|
||||
booleanFieldValueSchema,
|
||||
collaboratorFieldAttributesSchema,
|
||||
collaboratorFieldValueSchema,
|
||||
createdAtFieldAttributesSchema,
|
||||
dateFieldAttributesSchema,
|
||||
dateFieldValueSchema,
|
||||
emailFieldAttributesSchema,
|
||||
emailFieldValueSchema,
|
||||
numberFieldAttributesSchema,
|
||||
numberFieldValueSchema,
|
||||
phoneFieldAttributesSchema,
|
||||
phoneFieldValueSchema,
|
||||
textFieldAttributesSchema,
|
||||
textFieldValueSchema,
|
||||
urlFieldAttributesSchema,
|
||||
urlFieldValueSchema,
|
||||
updatedAtFieldAttributesSchema,
|
||||
updatedAtFieldValueSchema,
|
||||
updatedByFieldAttributesSchema,
|
||||
updatedByFieldValueSchema,
|
||||
fieldAttributesSchema,
|
||||
fieldValueSchema,
|
||||
BooleanFieldAttributes,
|
||||
BooleanFieldValue,
|
||||
CollaboratorFieldAttributes,
|
||||
CollaboratorFieldValue,
|
||||
CreatedAtFieldAttributes,
|
||||
CreatedAtFieldValue,
|
||||
DateFieldAttributes,
|
||||
DateFieldValue,
|
||||
EmailFieldAttributes,
|
||||
EmailFieldValue,
|
||||
FileFieldAttributes,
|
||||
FileFieldValue,
|
||||
MultiSelectFieldAttributes,
|
||||
MultiSelectFieldValue,
|
||||
NumberFieldAttributes,
|
||||
NumberFieldValue,
|
||||
PhoneFieldAttributes,
|
||||
PhoneFieldValue,
|
||||
RelationFieldAttributes,
|
||||
RelationFieldValue,
|
||||
RollupFieldAttributes,
|
||||
RollupFieldValue,
|
||||
SelectFieldAttributes,
|
||||
SelectFieldValue,
|
||||
TextFieldAttributes,
|
||||
TextFieldValue,
|
||||
UrlFieldAttributes,
|
||||
CreatedByFieldAttributes,
|
||||
CreatedByFieldValue,
|
||||
UpdatedByFieldAttributes,
|
||||
FieldAttributes,
|
||||
FieldValue,
|
||||
FieldType,
|
||||
SelectOptionAttributes,
|
||||
UpdatedAtFieldAttributes,
|
||||
UpdatedAtFieldValue,
|
||||
createdAtFieldValueSchema,
|
||||
createdByFieldAttributesSchema,
|
||||
createdByFieldValueSchema,
|
||||
fileFieldAttributesSchema,
|
||||
fileFieldValueSchema,
|
||||
multiSelectFieldAttributesSchema,
|
||||
multiSelectFieldValueSchema,
|
||||
relationFieldAttributesSchema,
|
||||
relationFieldValueSchema,
|
||||
rollupFieldAttributesSchema,
|
||||
rollupFieldValueSchema,
|
||||
selectFieldAttributesSchema,
|
||||
selectFieldValueSchema,
|
||||
} from './registry/fields';
|
||||
import { NodeRole, NodeModel, NodeMutationContext } from './registry/core';
|
||||
import {
|
||||
registry,
|
||||
NodeAttributes,
|
||||
ChannelNode,
|
||||
ChatNode,
|
||||
DatabaseNode,
|
||||
FileNode,
|
||||
FolderNode,
|
||||
MessageNode,
|
||||
PageNode,
|
||||
RecordNode,
|
||||
SpaceNode,
|
||||
UserNode,
|
||||
Node,
|
||||
} from './registry';
|
||||
|
||||
export {
|
||||
yjs,
|
||||
zod,
|
||||
registry,
|
||||
type ChannelAttributes,
|
||||
channelAttributesSchema,
|
||||
channelModel,
|
||||
type PageAttributes,
|
||||
pageAttributesSchema,
|
||||
pageModel,
|
||||
type ChatAttributes,
|
||||
chatAttributesSchema,
|
||||
chatModel,
|
||||
type SpaceAttributes,
|
||||
spaceAttributesSchema,
|
||||
spaceModel,
|
||||
type UserAttributes,
|
||||
userAttributesSchema,
|
||||
userModel,
|
||||
type MessageAttributes,
|
||||
messageAttributesSchema,
|
||||
messageModel,
|
||||
type ViewFieldAttributes,
|
||||
viewFieldAttributesSchema,
|
||||
type ViewFieldFilterAttributes,
|
||||
viewFieldFilterAttributesSchema,
|
||||
type ViewGroupFilterAttributes,
|
||||
viewGroupFilterAttributesSchema,
|
||||
type ViewFilterAttributes,
|
||||
viewFilterAttributesSchema,
|
||||
type ViewSortAttributes,
|
||||
viewSortAttributesSchema,
|
||||
type ViewAttributes,
|
||||
viewAttributesSchema,
|
||||
type DatabaseAttributes,
|
||||
databaseAttributesSchema,
|
||||
databaseModel,
|
||||
type FileAttributes,
|
||||
fileAttributesSchema,
|
||||
fileModel,
|
||||
type FolderAttributes,
|
||||
folderAttributesSchema,
|
||||
folderModel,
|
||||
type RecordAttributes,
|
||||
recordAttributesSchema,
|
||||
recordModel,
|
||||
blockLeafSchema,
|
||||
blockSchema,
|
||||
selectOptionAttributesSchema,
|
||||
booleanFieldAttributesSchema,
|
||||
booleanFieldValueSchema,
|
||||
collaboratorFieldAttributesSchema,
|
||||
collaboratorFieldValueSchema,
|
||||
createdAtFieldAttributesSchema,
|
||||
dateFieldAttributesSchema,
|
||||
dateFieldValueSchema,
|
||||
emailFieldAttributesSchema,
|
||||
emailFieldValueSchema,
|
||||
numberFieldAttributesSchema,
|
||||
numberFieldValueSchema,
|
||||
phoneFieldAttributesSchema,
|
||||
phoneFieldValueSchema,
|
||||
textFieldAttributesSchema,
|
||||
textFieldValueSchema,
|
||||
urlFieldAttributesSchema,
|
||||
urlFieldValueSchema,
|
||||
updatedAtFieldAttributesSchema,
|
||||
updatedAtFieldValueSchema,
|
||||
updatedByFieldAttributesSchema,
|
||||
updatedByFieldValueSchema,
|
||||
fieldAttributesSchema,
|
||||
fieldValueSchema,
|
||||
type BooleanFieldAttributes,
|
||||
type BooleanFieldValue,
|
||||
type CollaboratorFieldAttributes,
|
||||
type CollaboratorFieldValue,
|
||||
type CreatedAtFieldAttributes,
|
||||
type CreatedAtFieldValue,
|
||||
type DateFieldAttributes,
|
||||
type DateFieldValue,
|
||||
type EmailFieldAttributes,
|
||||
type EmailFieldValue,
|
||||
type FileFieldAttributes,
|
||||
type FileFieldValue,
|
||||
type MultiSelectFieldAttributes,
|
||||
type MultiSelectFieldValue,
|
||||
type NumberFieldAttributes,
|
||||
type NumberFieldValue,
|
||||
type PhoneFieldAttributes,
|
||||
type PhoneFieldValue,
|
||||
type RelationFieldAttributes,
|
||||
type RelationFieldValue,
|
||||
type RollupFieldAttributes,
|
||||
type RollupFieldValue,
|
||||
type SelectFieldAttributes,
|
||||
type SelectFieldValue,
|
||||
type TextFieldAttributes,
|
||||
type TextFieldValue,
|
||||
type UrlFieldAttributes,
|
||||
type CreatedByFieldAttributes,
|
||||
type CreatedByFieldValue,
|
||||
type UpdatedByFieldAttributes,
|
||||
type FieldAttributes,
|
||||
type FieldValue,
|
||||
type FieldType,
|
||||
type SelectOptionAttributes,
|
||||
type UpdatedAtFieldAttributes,
|
||||
type UpdatedAtFieldValue,
|
||||
createdAtFieldValueSchema,
|
||||
createdByFieldAttributesSchema,
|
||||
createdByFieldValueSchema,
|
||||
fileFieldAttributesSchema,
|
||||
fileFieldValueSchema,
|
||||
multiSelectFieldAttributesSchema,
|
||||
multiSelectFieldValueSchema,
|
||||
relationFieldAttributesSchema,
|
||||
relationFieldValueSchema,
|
||||
rollupFieldAttributesSchema,
|
||||
rollupFieldValueSchema,
|
||||
selectFieldAttributesSchema,
|
||||
selectFieldValueSchema,
|
||||
type NodeRole,
|
||||
type NodeModel,
|
||||
type NodeMutationContext,
|
||||
type NodeAttributes,
|
||||
type ChannelNode,
|
||||
type ChatNode,
|
||||
type DatabaseNode,
|
||||
type FileNode,
|
||||
type FolderNode,
|
||||
type MessageNode,
|
||||
type PageNode,
|
||||
type RecordNode,
|
||||
type SpaceNode,
|
||||
type UserNode,
|
||||
type BlockLeaf,
|
||||
type Block,
|
||||
type Node,
|
||||
applyCrdt,
|
||||
};
|
||||
|
||||
28
packages/core/src/registry/block.ts
Normal file
28
packages/core/src/registry/block.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ZodText } from './zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const blockLeafSchema = z.object({
|
||||
type: z.string(),
|
||||
text: ZodText.create(),
|
||||
marks: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.string(),
|
||||
attrs: z.record(z.any()).nullable(),
|
||||
})
|
||||
)
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
export type BlockLeaf = z.infer<typeof blockLeafSchema>;
|
||||
|
||||
export const blockSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
parentId: z.string(),
|
||||
content: z.array(blockLeafSchema).nullable(),
|
||||
attrs: z.record(z.any()).nullable(),
|
||||
index: z.string(),
|
||||
});
|
||||
|
||||
export type Block = z.infer<typeof blockSchema>;
|
||||
54
packages/core/src/registry/channel.ts
Normal file
54
packages/core/src/registry/channel.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { z } from 'zod';
|
||||
import { NodeModel } from './core';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
export const channelAttributesSchema = z.object({
|
||||
type: z.literal('channel'),
|
||||
name: z.string(),
|
||||
avatar: z.string().nullable().optional(),
|
||||
parentId: z.string(),
|
||||
index: z.string(),
|
||||
collaborators: z.record(z.string()).nullable().optional(),
|
||||
});
|
||||
|
||||
export type ChannelAttributes = z.infer<typeof channelAttributesSchema>;
|
||||
|
||||
export const channelModel: NodeModel = {
|
||||
type: 'channel',
|
||||
schema: channelAttributesSchema,
|
||||
canCreate: async (context, attributes) => {
|
||||
if (attributes.type !== 'channel') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.ancestors.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parent = context.ancestors[0];
|
||||
if (!parent || parent.type !== 'space') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const collaboratorIds = Object.keys(attributes.collaborators ?? {});
|
||||
if (collaboratorIds.length > 0 && !context.hasAdminAccess()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return context.hasEditorAccess();
|
||||
},
|
||||
canUpdate: async (context, node, attributes) => {
|
||||
if (attributes.type !== 'channel' || node.type !== 'channel') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isEqual(node.attributes.collaborators, attributes.collaborators)) {
|
||||
return context.hasAdminAccess();
|
||||
}
|
||||
|
||||
return context.hasEditorAccess();
|
||||
},
|
||||
canDelete: async (context, _) => {
|
||||
return context.hasEditorAccess();
|
||||
},
|
||||
};
|
||||
37
packages/core/src/registry/chat.ts
Normal file
37
packages/core/src/registry/chat.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { z } from 'zod';
|
||||
import { NodeModel } from './core';
|
||||
|
||||
export const chatAttributesSchema = z.object({
|
||||
type: z.literal('chat'),
|
||||
parentId: z.string(),
|
||||
collaborators: z.record(z.string()),
|
||||
});
|
||||
|
||||
export type ChatAttributes = z.infer<typeof chatAttributesSchema>;
|
||||
|
||||
export const chatModel: NodeModel = {
|
||||
type: 'chat',
|
||||
schema: chatAttributesSchema,
|
||||
canCreate: async (context, attributes) => {
|
||||
if (attributes.type !== 'chat') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const collaboratorIds = Object.keys(attributes.collaborators ?? {});
|
||||
if (collaboratorIds.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!collaboratorIds.includes(context.userId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
canUpdate: async () => {
|
||||
return false;
|
||||
},
|
||||
canDelete: async () => {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
31
packages/core/src/registry/core.ts
Normal file
31
packages/core/src/registry/core.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ZodSchema } from 'zod';
|
||||
import { Node, NodeAttributes } from './';
|
||||
|
||||
export type NodeRole = 'admin' | 'editor' | 'collaborator' | 'viewer';
|
||||
|
||||
export interface NodeMutationContext {
|
||||
accountId: string;
|
||||
workspaceId: string;
|
||||
userId: string;
|
||||
ancestors: Node[];
|
||||
role: NodeRole;
|
||||
hasAdminAccess: () => boolean;
|
||||
hasEditorAccess: () => boolean;
|
||||
hasCollaboratorAccess: () => boolean;
|
||||
hasViewerAccess: () => boolean;
|
||||
}
|
||||
|
||||
export interface NodeModel {
|
||||
type: string;
|
||||
schema: ZodSchema;
|
||||
canCreate: (
|
||||
context: NodeMutationContext,
|
||||
attributes: NodeAttributes
|
||||
) => Promise<boolean>;
|
||||
canUpdate: (
|
||||
context: NodeMutationContext,
|
||||
node: Node,
|
||||
attributes: NodeAttributes
|
||||
) => Promise<boolean>;
|
||||
canDelete: (context: NodeMutationContext, node: Node) => Promise<boolean>;
|
||||
}
|
||||
111
packages/core/src/registry/database.ts
Normal file
111
packages/core/src/registry/database.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { z } from 'zod';
|
||||
import { NodeModel } from './core';
|
||||
import { fieldAttributesSchema } from './fields';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
export const viewFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
width: z.number().nullable().optional(),
|
||||
display: z.boolean().nullable().optional(),
|
||||
index: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export type ViewFieldAttributes = z.infer<typeof viewFieldAttributesSchema>;
|
||||
|
||||
export const viewFieldFilterAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
fieldId: z.string(),
|
||||
type: z.literal('field'),
|
||||
operator: z.string(),
|
||||
value: z
|
||||
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
export type ViewFieldFilterAttributes = z.infer<
|
||||
typeof viewFieldFilterAttributesSchema
|
||||
>;
|
||||
|
||||
export const viewGroupFilterAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('group'),
|
||||
operator: z.enum(['and', 'or']),
|
||||
filters: z.array(viewFieldFilterAttributesSchema),
|
||||
});
|
||||
|
||||
export type ViewGroupFilterAttributes = z.infer<
|
||||
typeof viewGroupFilterAttributesSchema
|
||||
>;
|
||||
|
||||
export const viewSortAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
fieldId: z.string(),
|
||||
direction: z.enum(['asc', 'desc']),
|
||||
});
|
||||
|
||||
export type ViewSortAttributes = z.infer<typeof viewSortAttributesSchema>;
|
||||
|
||||
export const viewFilterAttributesSchema = z.discriminatedUnion('type', [
|
||||
viewFieldFilterAttributesSchema,
|
||||
viewGroupFilterAttributesSchema,
|
||||
]);
|
||||
|
||||
export type ViewFilterAttributes = z.infer<typeof viewFilterAttributesSchema>;
|
||||
|
||||
export const viewAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.enum(['table', 'board', 'calendar']),
|
||||
name: z.string(),
|
||||
avatar: z.string().nullable(),
|
||||
index: z.string(),
|
||||
fields: z.record(z.string(), viewFieldAttributesSchema),
|
||||
filters: z.record(z.string(), viewFilterAttributesSchema),
|
||||
sorts: z.record(z.string(), viewSortAttributesSchema),
|
||||
groupBy: z.string().nullable(),
|
||||
nameWidth: z.number().nullable().optional(),
|
||||
});
|
||||
|
||||
export type ViewAttributes = z.infer<typeof viewAttributesSchema>;
|
||||
|
||||
export const databaseAttributesSchema = z.object({
|
||||
type: z.literal('database'),
|
||||
name: z.string(),
|
||||
avatar: z.string().nullable().optional(),
|
||||
parentId: z.string(),
|
||||
collaborators: z.record(z.string()).nullable().optional(),
|
||||
fields: z.record(z.string(), fieldAttributesSchema),
|
||||
views: z.record(z.string(), viewAttributesSchema),
|
||||
});
|
||||
|
||||
export type DatabaseAttributes = z.infer<typeof databaseAttributesSchema>;
|
||||
|
||||
export const databaseModel: NodeModel = {
|
||||
type: 'database',
|
||||
schema: databaseAttributesSchema,
|
||||
canCreate: async (context, attributes) => {
|
||||
if (attributes.type !== 'database') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const collaboratorIds = Object.keys(attributes.collaborators ?? {});
|
||||
if (collaboratorIds.length > 0 && !context.hasAdminAccess()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return context.hasEditorAccess();
|
||||
},
|
||||
canUpdate: async (context, node, attributes) => {
|
||||
if (attributes.type !== 'database' || node.type !== 'database') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isEqual(node.attributes.collaborators, attributes.collaborators)) {
|
||||
return context.hasAdminAccess();
|
||||
}
|
||||
|
||||
return context.hasEditorAccess();
|
||||
},
|
||||
canDelete: async (context, _) => {
|
||||
return context.hasEditorAccess();
|
||||
},
|
||||
};
|
||||
347
packages/core/src/registry/fields.ts
Normal file
347
packages/core/src/registry/fields.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
import { z } from 'zod';
|
||||
import { ZodText } from './zod';
|
||||
|
||||
export const selectOptionAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
color: z.string(),
|
||||
index: z.string(),
|
||||
});
|
||||
|
||||
export type SelectOptionAttributes = z.infer<
|
||||
typeof selectOptionAttributesSchema
|
||||
>;
|
||||
|
||||
export const booleanFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('boolean'),
|
||||
name: z.string(),
|
||||
index: z.string(),
|
||||
});
|
||||
|
||||
export type BooleanFieldAttributes = z.infer<
|
||||
typeof booleanFieldAttributesSchema
|
||||
>;
|
||||
|
||||
export const booleanFieldValueSchema = z.object({
|
||||
type: z.literal('boolean'),
|
||||
value: z.boolean(),
|
||||
});
|
||||
|
||||
export type BooleanFieldValue = z.infer<typeof booleanFieldValueSchema>;
|
||||
|
||||
export const collaboratorFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('collaborator'),
|
||||
name: z.string(),
|
||||
index: z.string(),
|
||||
});
|
||||
|
||||
export type CollaboratorFieldAttributes = z.infer<
|
||||
typeof collaboratorFieldAttributesSchema
|
||||
>;
|
||||
|
||||
export const collaboratorFieldValueSchema = z.object({
|
||||
type: z.literal('collaborator'),
|
||||
value: z.array(z.string()),
|
||||
});
|
||||
|
||||
export type CollaboratorFieldValue = z.infer<
|
||||
typeof collaboratorFieldValueSchema
|
||||
>;
|
||||
|
||||
export const createdAtFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('createdAt'),
|
||||
name: z.string(),
|
||||
index: z.string(),
|
||||
});
|
||||
|
||||
export type CreatedAtFieldAttributes = z.infer<
|
||||
typeof createdAtFieldAttributesSchema
|
||||
>;
|
||||
|
||||
export const createdAtFieldValueSchema = z.object({
|
||||
type: z.literal('createdAt'),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export type CreatedAtFieldValue = z.infer<typeof createdAtFieldValueSchema>;
|
||||
|
||||
export const createdByFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('createdBy'),
|
||||
name: z.string(),
|
||||
index: z.string(),
|
||||
});
|
||||
|
||||
export type CreatedByFieldAttributes = z.infer<
|
||||
typeof createdByFieldAttributesSchema
|
||||
>;
|
||||
|
||||
export const createdByFieldValueSchema = z.object({
|
||||
type: z.literal('createdBy'),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export type CreatedByFieldValue = z.infer<typeof createdByFieldValueSchema>;
|
||||
|
||||
export const dateFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('date'),
|
||||
name: z.string(),
|
||||
index: z.string(),
|
||||
});
|
||||
|
||||
export type DateFieldAttributes = z.infer<typeof dateFieldAttributesSchema>;
|
||||
|
||||
export const dateFieldValueSchema = z.object({
|
||||
type: z.literal('date'),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export type DateFieldValue = z.infer<typeof dateFieldValueSchema>;
|
||||
|
||||
export const emailFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('email'),
|
||||
name: z.string(),
|
||||
index: z.string(),
|
||||
});
|
||||
|
||||
export type EmailFieldAttributes = z.infer<typeof emailFieldAttributesSchema>;
|
||||
|
||||
export const emailFieldValueSchema = z.object({
|
||||
type: z.literal('email'),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export type EmailFieldValue = z.infer<typeof emailFieldValueSchema>;
|
||||
|
||||
export const fileFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('file'),
|
||||
name: z.string(),
|
||||
index: z.string(),
|
||||
});
|
||||
|
||||
export type FileFieldAttributes = z.infer<typeof fileFieldAttributesSchema>;
|
||||
|
||||
export const fileFieldValueSchema = z.object({
|
||||
type: z.literal('file'),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export type FileFieldValue = z.infer<typeof fileFieldValueSchema>;
|
||||
|
||||
export const multiSelectFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('multiSelect'),
|
||||
name: z.string(),
|
||||
index: z.string(),
|
||||
options: z.record(z.string(), selectOptionAttributesSchema).optional(),
|
||||
});
|
||||
|
||||
export type MultiSelectFieldAttributes = z.infer<
|
||||
typeof multiSelectFieldAttributesSchema
|
||||
>;
|
||||
|
||||
export const multiSelectFieldValueSchema = z.object({
|
||||
type: z.literal('multiSelect'),
|
||||
value: z.array(z.string()),
|
||||
});
|
||||
|
||||
export type MultiSelectFieldValue = z.infer<typeof multiSelectFieldValueSchema>;
|
||||
|
||||
export const numberFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('number'),
|
||||
name: z.string(),
|
||||
index: z.string(),
|
||||
});
|
||||
|
||||
export type NumberFieldAttributes = z.infer<typeof numberFieldAttributesSchema>;
|
||||
|
||||
export const numberFieldValueSchema = z.object({
|
||||
type: z.literal('number'),
|
||||
value: z.number(),
|
||||
});
|
||||
|
||||
export type NumberFieldValue = z.infer<typeof numberFieldValueSchema>;
|
||||
|
||||
export const phoneFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('phone'),
|
||||
name: z.string(),
|
||||
index: z.string(),
|
||||
});
|
||||
|
||||
export type PhoneFieldAttributes = z.infer<typeof phoneFieldAttributesSchema>;
|
||||
|
||||
export const phoneFieldValueSchema = z.object({
|
||||
type: z.literal('phone'),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export type PhoneFieldValue = z.infer<typeof phoneFieldValueSchema>;
|
||||
|
||||
export const relationFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('relation'),
|
||||
name: z.string(),
|
||||
index: z.string(),
|
||||
});
|
||||
|
||||
export type RelationFieldAttributes = z.infer<
|
||||
typeof relationFieldAttributesSchema
|
||||
>;
|
||||
|
||||
export const relationFieldValueSchema = z.object({
|
||||
type: z.literal('relation'),
|
||||
value: z.array(z.string()),
|
||||
});
|
||||
|
||||
export type RelationFieldValue = z.infer<typeof relationFieldValueSchema>;
|
||||
|
||||
export const rollupFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('rollup'),
|
||||
name: z.string(),
|
||||
index: z.string(),
|
||||
});
|
||||
|
||||
export type RollupFieldAttributes = z.infer<typeof rollupFieldAttributesSchema>;
|
||||
|
||||
export const rollupFieldValueSchema = z.object({
|
||||
type: z.literal('rollup'),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export type RollupFieldValue = z.infer<typeof rollupFieldValueSchema>;
|
||||
|
||||
export const selectFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('select'),
|
||||
name: z.string(),
|
||||
index: z.string(),
|
||||
options: z.record(z.string(), selectOptionAttributesSchema).optional(),
|
||||
});
|
||||
|
||||
export type SelectFieldAttributes = z.infer<typeof selectFieldAttributesSchema>;
|
||||
|
||||
export const selectFieldValueSchema = z.object({
|
||||
type: z.literal('select'),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export type SelectFieldValue = z.infer<typeof selectFieldValueSchema>;
|
||||
|
||||
export const textFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('text'),
|
||||
name: z.string(),
|
||||
index: z.string(),
|
||||
});
|
||||
|
||||
export type TextFieldAttributes = z.infer<typeof textFieldAttributesSchema>;
|
||||
|
||||
export const textFieldValueSchema = z.object({
|
||||
type: z.literal('text'),
|
||||
value: ZodText.create(),
|
||||
});
|
||||
|
||||
export type TextFieldValue = z.infer<typeof textFieldValueSchema>;
|
||||
|
||||
export const urlFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('url'),
|
||||
name: z.string(),
|
||||
index: z.string(),
|
||||
});
|
||||
|
||||
export type UrlFieldAttributes = z.infer<typeof urlFieldAttributesSchema>;
|
||||
|
||||
export const urlFieldValueSchema = z.object({
|
||||
type: z.literal('url'),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export const updatedAtFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('updatedAt'),
|
||||
name: z.string(),
|
||||
index: z.string(),
|
||||
});
|
||||
|
||||
export type UpdatedAtFieldAttributes = z.infer<
|
||||
typeof updatedAtFieldAttributesSchema
|
||||
>;
|
||||
|
||||
export const updatedAtFieldValueSchema = z.object({
|
||||
type: z.literal('updatedAt'),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export type UpdatedAtFieldValue = z.infer<typeof updatedAtFieldValueSchema>;
|
||||
|
||||
export const updatedByFieldAttributesSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('updatedBy'),
|
||||
name: z.string(),
|
||||
index: z.string(),
|
||||
});
|
||||
|
||||
export type UpdatedByFieldAttributes = z.infer<
|
||||
typeof updatedByFieldAttributesSchema
|
||||
>;
|
||||
|
||||
export const updatedByFieldValueSchema = z.object({
|
||||
type: z.literal('updatedBy'),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export const fieldAttributesSchema = z.discriminatedUnion('type', [
|
||||
booleanFieldAttributesSchema,
|
||||
collaboratorFieldAttributesSchema,
|
||||
createdAtFieldAttributesSchema,
|
||||
createdByFieldAttributesSchema,
|
||||
dateFieldAttributesSchema,
|
||||
emailFieldAttributesSchema,
|
||||
fileFieldAttributesSchema,
|
||||
multiSelectFieldAttributesSchema,
|
||||
numberFieldAttributesSchema,
|
||||
phoneFieldAttributesSchema,
|
||||
relationFieldAttributesSchema,
|
||||
rollupFieldAttributesSchema,
|
||||
selectFieldAttributesSchema,
|
||||
textFieldAttributesSchema,
|
||||
urlFieldAttributesSchema,
|
||||
updatedAtFieldAttributesSchema,
|
||||
updatedByFieldAttributesSchema,
|
||||
]);
|
||||
|
||||
export type FieldAttributes = z.infer<typeof fieldAttributesSchema>;
|
||||
|
||||
export const fieldValueSchema = z.discriminatedUnion('type', [
|
||||
booleanFieldValueSchema,
|
||||
collaboratorFieldValueSchema,
|
||||
createdAtFieldValueSchema,
|
||||
createdByFieldValueSchema,
|
||||
dateFieldValueSchema,
|
||||
emailFieldValueSchema,
|
||||
fileFieldValueSchema,
|
||||
multiSelectFieldValueSchema,
|
||||
numberFieldValueSchema,
|
||||
phoneFieldValueSchema,
|
||||
relationFieldValueSchema,
|
||||
rollupFieldValueSchema,
|
||||
selectFieldValueSchema,
|
||||
textFieldValueSchema,
|
||||
urlFieldValueSchema,
|
||||
updatedAtFieldValueSchema,
|
||||
updatedByFieldValueSchema,
|
||||
]);
|
||||
|
||||
export type FieldValue = z.infer<typeof fieldValueSchema>;
|
||||
|
||||
export type FieldType = Extract<FieldAttributes['type'], string>;
|
||||
28
packages/core/src/registry/file.ts
Normal file
28
packages/core/src/registry/file.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { z } from 'zod';
|
||||
import { NodeModel } from './core';
|
||||
|
||||
export const fileAttributesSchema = z.object({
|
||||
type: z.literal('file'),
|
||||
name: z.string(),
|
||||
parentId: z.string(),
|
||||
mimeType: z.string(),
|
||||
size: z.number(),
|
||||
extension: z.string(),
|
||||
fileName: z.string(),
|
||||
});
|
||||
|
||||
export type FileAttributes = z.infer<typeof fileAttributesSchema>;
|
||||
|
||||
export const fileModel: NodeModel = {
|
||||
type: 'file',
|
||||
schema: fileAttributesSchema,
|
||||
canCreate: async (_, __) => {
|
||||
return true;
|
||||
},
|
||||
canUpdate: async (_, __, ___) => {
|
||||
return true;
|
||||
},
|
||||
canDelete: async (_, __) => {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
44
packages/core/src/registry/folder.ts
Normal file
44
packages/core/src/registry/folder.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { z } from 'zod';
|
||||
import { NodeModel } from './core';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
export const folderAttributesSchema = z.object({
|
||||
type: z.literal('folder'),
|
||||
name: z.string(),
|
||||
avatar: z.string().nullable().optional(),
|
||||
parentId: z.string(),
|
||||
collaborators: z.record(z.string()).nullable().optional(),
|
||||
});
|
||||
|
||||
export type FolderAttributes = z.infer<typeof folderAttributesSchema>;
|
||||
|
||||
export const folderModel: NodeModel = {
|
||||
type: 'folder',
|
||||
schema: folderAttributesSchema,
|
||||
canCreate: async (context, attributes) => {
|
||||
if (attributes.type !== 'folder') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const collaboratorIds = Object.keys(attributes.collaborators ?? {});
|
||||
if (collaboratorIds.length > 0 && !context.hasAdminAccess()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return context.hasEditorAccess();
|
||||
},
|
||||
canUpdate: async (context, node, attributes) => {
|
||||
if (attributes.type !== 'folder' || node.type !== 'folder') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isEqual(node.attributes.collaborators, attributes.collaborators)) {
|
||||
return context.hasAdminAccess();
|
||||
}
|
||||
|
||||
return context.hasEditorAccess();
|
||||
},
|
||||
canDelete: async (context, _) => {
|
||||
return context.hasEditorAccess();
|
||||
},
|
||||
};
|
||||
112
packages/core/src/registry/index.ts
Normal file
112
packages/core/src/registry/index.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { ChannelAttributes, channelModel } from './channel';
|
||||
import { NodeModel } from './core';
|
||||
import { PageAttributes, pageModel } from './page';
|
||||
import { ChatAttributes, chatModel } from './chat';
|
||||
import { SpaceAttributes, spaceModel } from './space';
|
||||
import { UserAttributes, userModel } from './user';
|
||||
import { MessageAttributes, messageModel } from './message';
|
||||
import { DatabaseAttributes, databaseModel } from './database';
|
||||
import { FileAttributes, fileModel } from './file';
|
||||
import { FolderAttributes, folderModel } from './folder';
|
||||
import { RecordAttributes, recordModel } from './record';
|
||||
|
||||
export const registry: Record<string, NodeModel> = {
|
||||
channel: channelModel,
|
||||
chat: chatModel,
|
||||
database: databaseModel,
|
||||
file: fileModel,
|
||||
folder: folderModel,
|
||||
message: messageModel,
|
||||
page: pageModel,
|
||||
record: recordModel,
|
||||
space: spaceModel,
|
||||
user: userModel,
|
||||
};
|
||||
|
||||
type NodeBase = {
|
||||
id: string;
|
||||
parentId: string | null;
|
||||
index: string | null;
|
||||
createdAt: string;
|
||||
createdBy: string;
|
||||
updatedAt: string | null;
|
||||
updatedBy: string | null;
|
||||
versionId: string;
|
||||
serverCreatedAt: string | null;
|
||||
serverUpdatedAt: string | null;
|
||||
serverVersionId: string | null;
|
||||
};
|
||||
|
||||
export type ChannelNode = NodeBase & {
|
||||
type: 'channel';
|
||||
attributes: ChannelAttributes;
|
||||
};
|
||||
|
||||
export type ChatNode = NodeBase & {
|
||||
type: 'chat';
|
||||
attributes: ChatAttributes;
|
||||
};
|
||||
|
||||
export type DatabaseNode = NodeBase & {
|
||||
type: 'database';
|
||||
attributes: DatabaseAttributes;
|
||||
};
|
||||
|
||||
export type FileNode = NodeBase & {
|
||||
type: 'file';
|
||||
attributes: FileAttributes;
|
||||
};
|
||||
|
||||
export type FolderNode = NodeBase & {
|
||||
type: 'folder';
|
||||
attributes: FolderAttributes;
|
||||
};
|
||||
|
||||
export type MessageNode = NodeBase & {
|
||||
type: 'message';
|
||||
attributes: MessageAttributes;
|
||||
};
|
||||
|
||||
export type PageNode = NodeBase & {
|
||||
type: 'page';
|
||||
attributes: PageAttributes;
|
||||
};
|
||||
|
||||
export type RecordNode = NodeBase & {
|
||||
type: 'record';
|
||||
attributes: RecordAttributes;
|
||||
};
|
||||
|
||||
export type SpaceNode = NodeBase & {
|
||||
type: 'space';
|
||||
attributes: SpaceAttributes;
|
||||
};
|
||||
|
||||
export type UserNode = NodeBase & {
|
||||
type: 'user';
|
||||
attributes: UserAttributes;
|
||||
};
|
||||
|
||||
export type Node =
|
||||
| ChannelNode
|
||||
| ChatNode
|
||||
| DatabaseNode
|
||||
| FileNode
|
||||
| FolderNode
|
||||
| MessageNode
|
||||
| PageNode
|
||||
| RecordNode
|
||||
| SpaceNode
|
||||
| UserNode;
|
||||
|
||||
export type NodeAttributes =
|
||||
| UserAttributes
|
||||
| SpaceAttributes
|
||||
| DatabaseAttributes
|
||||
| ChannelAttributes
|
||||
| ChatAttributes
|
||||
| FileAttributes
|
||||
| FolderAttributes
|
||||
| MessageAttributes
|
||||
| PageAttributes
|
||||
| RecordAttributes;
|
||||
47
packages/core/src/registry/message.ts
Normal file
47
packages/core/src/registry/message.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { z } from 'zod';
|
||||
import { NodeModel } from './core';
|
||||
import { blockSchema } from './block';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
export const messageAttributesSchema = z.object({
|
||||
type: z.literal('message'),
|
||||
parentId: z.string().nullable(),
|
||||
content: z.record(z.string(), blockSchema),
|
||||
reactions: z.record(z.array(z.string())),
|
||||
});
|
||||
|
||||
export type MessageAttributes = z.infer<typeof messageAttributesSchema>;
|
||||
|
||||
export const messageModel: NodeModel = {
|
||||
type: 'message',
|
||||
schema: messageAttributesSchema,
|
||||
canCreate: async (context, attributes) => {
|
||||
if (attributes.type !== 'message') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return context.hasCollaboratorAccess();
|
||||
},
|
||||
canUpdate: async (context, node, attributes) => {
|
||||
if (attributes.type !== 'message' || node.type !== 'message') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isEqual(attributes.content, node.attributes.content)) {
|
||||
return context.userId === node.createdBy;
|
||||
}
|
||||
|
||||
return context.hasCollaboratorAccess();
|
||||
},
|
||||
canDelete: async (context, node) => {
|
||||
if (node.type !== 'message') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.userId === node.createdBy) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return context.hasAdminAccess();
|
||||
},
|
||||
};
|
||||
46
packages/core/src/registry/page.ts
Normal file
46
packages/core/src/registry/page.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { z } from 'zod';
|
||||
import { NodeModel } from './core';
|
||||
import { blockSchema } from './block';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
export const pageAttributesSchema = z.object({
|
||||
type: z.literal('page'),
|
||||
name: z.string(),
|
||||
avatar: z.string().nullable().optional(),
|
||||
parentId: z.string(),
|
||||
content: z.record(blockSchema),
|
||||
collaborators: z.record(z.string()).nullable().optional(),
|
||||
});
|
||||
|
||||
export type PageAttributes = z.infer<typeof pageAttributesSchema>;
|
||||
|
||||
export const pageModel: NodeModel = {
|
||||
type: 'page',
|
||||
schema: pageAttributesSchema,
|
||||
canCreate: async (context, attributes) => {
|
||||
if (attributes.type !== 'page') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const collaboratorIds = Object.keys(attributes.collaborators ?? {});
|
||||
if (collaboratorIds.length > 0 && !context.hasAdminAccess()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return context.hasEditorAccess();
|
||||
},
|
||||
canUpdate: async (context, node, attributes) => {
|
||||
if (attributes.type !== 'page' || node.type !== 'page') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isEqual(attributes.collaborators, node.attributes.collaborators)) {
|
||||
return context.hasAdminAccess();
|
||||
}
|
||||
|
||||
return context.hasEditorAccess();
|
||||
},
|
||||
canDelete: async (context, _) => {
|
||||
return context.hasEditorAccess();
|
||||
},
|
||||
};
|
||||
38
packages/core/src/registry/record.ts
Normal file
38
packages/core/src/registry/record.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { z } from 'zod';
|
||||
import { NodeModel } from './core';
|
||||
import { fieldValueSchema } from './fields';
|
||||
import { blockSchema } from './block';
|
||||
|
||||
export const recordAttributesSchema = z.object({
|
||||
type: z.literal('record'),
|
||||
parentId: z.string(),
|
||||
databaseId: z.string(),
|
||||
name: z.string(),
|
||||
avatar: z.string().nullable().optional(),
|
||||
fields: z.record(z.string(), fieldValueSchema),
|
||||
content: z.record(blockSchema),
|
||||
});
|
||||
|
||||
export type RecordAttributes = z.infer<typeof recordAttributesSchema>;
|
||||
|
||||
export const recordModel: NodeModel = {
|
||||
type: 'record',
|
||||
schema: recordAttributesSchema,
|
||||
canCreate: async (context, attributes) => {
|
||||
if (attributes.type !== 'record') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return context.hasCollaboratorAccess();
|
||||
},
|
||||
canUpdate: async (context, node, attributes) => {
|
||||
if (attributes.type !== 'record' || node.type !== 'record') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return context.hasCollaboratorAccess();
|
||||
},
|
||||
canDelete: async (context, _) => {
|
||||
return context.hasCollaboratorAccess();
|
||||
},
|
||||
};
|
||||
27
packages/core/src/registry/space.ts
Normal file
27
packages/core/src/registry/space.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { z } from 'zod';
|
||||
import { NodeModel } from './core';
|
||||
|
||||
export const spaceAttributesSchema = z.object({
|
||||
type: z.literal('space'),
|
||||
name: z.string(),
|
||||
parentId: z.string(),
|
||||
description: z.string().nullable(),
|
||||
avatar: z.string().nullable().optional(),
|
||||
collaborators: z.record(z.string()),
|
||||
});
|
||||
|
||||
export type SpaceAttributes = z.infer<typeof spaceAttributesSchema>;
|
||||
|
||||
export const spaceModel: NodeModel = {
|
||||
type: 'space',
|
||||
schema: spaceAttributesSchema,
|
||||
canCreate: async (_, __) => {
|
||||
return true;
|
||||
},
|
||||
canUpdate: async (_, __, ___) => {
|
||||
return true;
|
||||
},
|
||||
canDelete: async (_, __) => {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
28
packages/core/src/registry/user.ts
Normal file
28
packages/core/src/registry/user.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { z } from 'zod';
|
||||
import { NodeModel } from './core';
|
||||
|
||||
export const userAttributesSchema = z.object({
|
||||
type: z.literal('user'),
|
||||
name: z.string(),
|
||||
parentId: z.string(),
|
||||
email: z.string().email(),
|
||||
avatar: z.string().nullable(),
|
||||
accountId: z.string(),
|
||||
role: z.enum(['admin', 'editor', 'collaborator', 'viewer']),
|
||||
});
|
||||
|
||||
export type UserAttributes = z.infer<typeof userAttributesSchema>;
|
||||
|
||||
export const userModel: NodeModel = {
|
||||
type: 'user',
|
||||
schema: userAttributesSchema,
|
||||
canCreate: async (_, __) => {
|
||||
return true;
|
||||
},
|
||||
canUpdate: async (_, __, ___) => {
|
||||
return true;
|
||||
},
|
||||
canDelete: async (_, __) => {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
28
packages/core/src/registry/zod.ts
Normal file
28
packages/core/src/registry/zod.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
ZodType,
|
||||
ZodTypeDef,
|
||||
ParseInput,
|
||||
ParseReturnType,
|
||||
INVALID,
|
||||
OK,
|
||||
} from 'zod';
|
||||
|
||||
export class ZodText extends ZodType<string, ZodTypeDef, string> {
|
||||
constructor() {
|
||||
super({
|
||||
description: 'Text',
|
||||
});
|
||||
}
|
||||
|
||||
_parse(input: ParseInput): ParseReturnType<string> {
|
||||
if (typeof input.data !== 'string') {
|
||||
return INVALID;
|
||||
}
|
||||
|
||||
return OK(input.data);
|
||||
}
|
||||
|
||||
static create(): ZodText {
|
||||
return new ZodText();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,6 @@
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"composite": true
|
||||
}
|
||||
"composite": true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import path from 'node:path';
|
||||
import { configDefaults, defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
exclude: [...configDefaults.exclude],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user