diff --git a/apps/desktop/src/main/services/mutation-service.ts b/apps/desktop/src/main/services/mutation-service.ts index 333ae397..dc98af1c 100644 --- a/apps/desktop/src/main/services/mutation-service.ts +++ b/apps/desktop/src/main/services/mutation-service.ts @@ -27,6 +27,7 @@ class MutationService { const output = await handler.handleMutation(input); return { success: true, output }; } catch (error) { + this.debug(`Error executing mutation: ${input.type}`, error); if (error instanceof MutationError) { return { success: false, diff --git a/package-lock.json b/package-lock.json index 3a9196c7..2d87aaa4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2898,6 +2898,23 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@faker-js/faker": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.3.0.tgz", + "integrity": "sha512-r0tJ3ZOkMd9xsu3VRfqlFR6cz0V/jFYRswAIpC+m/DIfAUXq7g8N7wTAlhSANySXYGKzGryfDXwtwsY8TxEIDw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, "node_modules/@floating-ui/core": { "version": "1.6.8", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", @@ -7721,9 +7738,9 @@ } }, "node_modules/axios": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz", - "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -20934,12 +20951,17 @@ "name": "@colanode/scripts", "version": "1.0.0", "dependencies": { + "@colanode/core": "*", + "@colanode/crdt": "*", "adm-zip": "^0.5.16", - "node-fetch": "^3.3.2", - "ulid": "^2.3.0" + "axios": "^1.7.9", + "form-data": "^4.0.1", + "node-fetch": "^3.3.2" }, "devDependencies": { - "@types/adm-zip": "^0.5.7" + "@faker-js/faker": "^9.3.0", + "@types/adm-zip": "^0.5.7", + "tsx": "^4.19.2" } }, "scripts/node_modules/node-fetch": { diff --git a/scripts/package.json b/scripts/package.json index 3287a4f8..9b4b65ce 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -5,15 +5,21 @@ "private": true, "type": "module", "scripts": { - "generate:emojis": "node --no-warnings --loader ts-node/esm src/emojis/index.ts", - "generate:icons": "node --no-warnings --loader ts-node/esm src/icons/index.ts" + "generate:emojis": "tsx src/emojis/index.ts", + "generate:icons": "tsx src/icons/index.ts", + "seed": "tsx src/seed/index.ts" }, "devDependencies": { - "@types/adm-zip": "^0.5.7" + "@faker-js/faker": "^9.3.0", + "@types/adm-zip": "^0.5.7", + "tsx": "^4.19.2" }, "dependencies": { + "@colanode/core": "*", + "@colanode/crdt": "*", "adm-zip": "^0.5.16", - "node-fetch": "^3.3.2", - "ulid": "^2.3.0" + "axios": "^1.7.9", + "form-data": "^4.0.1", + "node-fetch": "^3.3.2" } } diff --git a/scripts/src/emojis/index.ts b/scripts/src/emojis/index.ts index 0c9e2990..37aa3e2f 100644 --- a/scripts/src/emojis/index.ts +++ b/scripts/src/emojis/index.ts @@ -1,11 +1,9 @@ import AdmZip from 'adm-zip'; import fetch from 'node-fetch'; -import { monotonicFactory } from 'ulid'; +import { generateId, IdType } from '@colanode/core'; import fs from 'fs'; -const ulid = monotonicFactory(); - type EmojiMartI18n = { categories: Record; }; @@ -77,10 +75,6 @@ type EmojiSkin = { unified: string; }; -const generateEmojiId = () => { - return ulid().toLowerCase() + 'em'; -}; - const downloadEmojiMartRepo = async () => { console.log(`Downloading emoji-mart repo`); const url = `${GITHUB_DOMAIN}/${EMOJI_MART_REPO}/archive/refs/tags/v${EMOJI_MART_TAG}.zip`; @@ -178,7 +172,7 @@ const generateEmojisDir = () => { (emoji) => emoji.code === emojiMartItem.id ); - const emojiId = existingEmoji ? existingEmoji.id : generateEmojiId(); + const emojiId = existingEmoji ? existingEmoji.id : generateId(IdType.Emoji); const emoji: Emoji = { id: emojiId, code: emojiMartItem.id, @@ -193,7 +187,7 @@ const generateEmojisDir = () => { (s) => s.unified === skin.unified ); - const skinId = existingSkin?.id ?? generateEmojiId(); + const skinId = existingSkin?.id ?? generateId(IdType.Emoji); emoji.skins.push({ id: skinId, unified: skin.unified, diff --git a/scripts/src/icons/index.ts b/scripts/src/icons/index.ts index 9bbc1cc2..c02cac11 100644 --- a/scripts/src/icons/index.ts +++ b/scripts/src/icons/index.ts @@ -1,11 +1,9 @@ import AdmZip from 'adm-zip'; import fetch from 'node-fetch'; -import { monotonicFactory } from 'ulid'; +import { generateId, IdType } from '@colanode/core'; import fs from 'fs'; -const ulid = monotonicFactory(); - type SimpleIconsData = { icons: SimpleIconItem[]; }; @@ -86,10 +84,6 @@ type IconCategory = { icons: string[]; }; -const generateIconId = () => { - return ulid().toLowerCase() + 'ic'; -}; - const downloadRemixIconRepo = async () => { console.log(`Downloading remix icon repo`); const url = `${GITHUB_DOMAIN}/${REMIX_ICON_REPO}/archive/refs/tags/v${REMIX_ICON_TAG}.zip`; @@ -189,7 +183,7 @@ const generateIconsDir = async () => { } } - const iconId = existingIcon ? existingIcon.id : generateIconId(); + const iconId = existingIcon ? existingIcon.id : generateId(IdType.Icon); const icon: Icon = { id: iconId, name: iconName, @@ -237,7 +231,7 @@ const generateIconsDir = async () => { simpleIconSlug, ]); - const iconId = existingIcon ? existingIcon.id : generateIconId(); + const iconId = existingIcon ? existingIcon.id : generateId(IdType.Icon); const icon: Icon = { id: iconId, name: simpleIconTitle, diff --git a/scripts/src/seed/accounts.json b/scripts/src/seed/accounts.json new file mode 100644 index 00000000..46321f5b --- /dev/null +++ b/scripts/src/seed/accounts.json @@ -0,0 +1,62 @@ +[ + { + "name": "Daniel Sykes", + "email": "daniel.sykes@example.com", + "password": "DanielSykes", + "avatar": "daniel_sykes.webp" + }, + { + "name": "Mark Lynch", + "email": "mark.lynch@example.com", + "password": "MarkLynch", + "avatar": "mark_lynch.webp" + }, + { + "name": "Scott Wright", + "email": "scott.wright@example.com", + "password": "ScottWright", + "avatar": "scott_wright.webp" + }, + { + "name": "James Isaacs", + "email": "james.isaacs@example.com", + "password": "JamesIsaacs", + "avatar": "james_isaacs.webp" + }, + { + "name": "Marlin Clarke", + "email": "marlin.clarke@example.com", + "password": "MarlinClarke", + "avatar": "marlin_clarke.webp" + }, + { + "name": "Patricia Ramirez", + "email": "patricia.ramirez@example.com", + "password": "PatriciaRamirez", + "avatar": "patricia_ramirez.webp" + }, + { + "name": "Linda Lira", + "email": "linda.lira@example.com", + "password": "LindaLira", + "avatar": "linda_lira.webp" + }, + { + "name": "Kimberly Burnette", + "email": "kimberly.burnette@example.com", + "password": "KimberlyBurnette", + "avatar": "kimberly_burnette.webp" + }, + { + "name": "Clara Lewis", + "email": "clara.lewis@example.com", + "password": "ClaraLewis", + "avatar": "clara_lewis.webp" + }, + { + "name": "Verda Rowell", + "email": "verda.rowell@example.com", + "password": "VerdaRowell", + "avatar": "verda_rowell.webp" + } +] diff --git a/scripts/src/seed/avatars/clara_lewis.webp b/scripts/src/seed/avatars/clara_lewis.webp new file mode 100644 index 00000000..749039f8 Binary files /dev/null and b/scripts/src/seed/avatars/clara_lewis.webp differ diff --git a/scripts/src/seed/avatars/daniel_sykes.webp b/scripts/src/seed/avatars/daniel_sykes.webp new file mode 100644 index 00000000..86327be6 Binary files /dev/null and b/scripts/src/seed/avatars/daniel_sykes.webp differ diff --git a/scripts/src/seed/avatars/james_isaacs.webp b/scripts/src/seed/avatars/james_isaacs.webp new file mode 100644 index 00000000..a234b002 Binary files /dev/null and b/scripts/src/seed/avatars/james_isaacs.webp differ diff --git a/scripts/src/seed/avatars/kimberly_burnette.webp b/scripts/src/seed/avatars/kimberly_burnette.webp new file mode 100644 index 00000000..5e79f4f9 Binary files /dev/null and b/scripts/src/seed/avatars/kimberly_burnette.webp differ diff --git a/scripts/src/seed/avatars/linda_lira.webp b/scripts/src/seed/avatars/linda_lira.webp new file mode 100644 index 00000000..20d48356 Binary files /dev/null and b/scripts/src/seed/avatars/linda_lira.webp differ diff --git a/scripts/src/seed/avatars/mark_lynch.webp b/scripts/src/seed/avatars/mark_lynch.webp new file mode 100644 index 00000000..82949fde Binary files /dev/null and b/scripts/src/seed/avatars/mark_lynch.webp differ diff --git a/scripts/src/seed/avatars/marlin_clarke.webp b/scripts/src/seed/avatars/marlin_clarke.webp new file mode 100644 index 00000000..d495428c Binary files /dev/null and b/scripts/src/seed/avatars/marlin_clarke.webp differ diff --git a/scripts/src/seed/avatars/patricia_ramirez.webp b/scripts/src/seed/avatars/patricia_ramirez.webp new file mode 100644 index 00000000..b3a3f413 Binary files /dev/null and b/scripts/src/seed/avatars/patricia_ramirez.webp differ diff --git a/scripts/src/seed/avatars/scott_wright.webp b/scripts/src/seed/avatars/scott_wright.webp new file mode 100644 index 00000000..57310a5f Binary files /dev/null and b/scripts/src/seed/avatars/scott_wright.webp differ diff --git a/scripts/src/seed/avatars/verda_rowell.webp b/scripts/src/seed/avatars/verda_rowell.webp new file mode 100644 index 00000000..2f4faaac Binary files /dev/null and b/scripts/src/seed/avatars/verda_rowell.webp differ diff --git a/scripts/src/seed/avatars/workspace_avatar.png b/scripts/src/seed/avatars/workspace_avatar.png new file mode 100644 index 00000000..cf122fdf Binary files /dev/null and b/scripts/src/seed/avatars/workspace_avatar.png differ diff --git a/scripts/src/seed/index.ts b/scripts/src/seed/index.ts new file mode 100644 index 00000000..2da33d1f --- /dev/null +++ b/scripts/src/seed/index.ts @@ -0,0 +1,223 @@ +import FormData from 'form-data'; +import axios from 'axios'; +import { LoginOutput } from '@colanode/core'; + +import fs from 'fs'; +import path from 'path'; + +import { FakerAccount, User } from './types'; +import { NodeGenerator } from './node-generator'; + +const SERVER_DOMAIN = 'http://localhost:3000'; + +const uploadAvatar = async ( + token: string, + avatarPath: string +): Promise => { + const avatarStream = fs.createReadStream(avatarPath); + + const formData = new FormData(); + formData.append('avatar', avatarStream); + + try { + const { data } = await axios.post<{ id: string }>( + `${SERVER_DOMAIN}/client/v1/avatars`, + formData, + { + headers: { + Authorization: `Bearer ${token}`, + ...formData.getHeaders(), + }, + } + ); + + return data.id; + } catch (error) { + console.error('Error uploading avatar:', error); + throw error; + } +}; + +const createAccount = async (account: FakerAccount): Promise => { + const url = `${SERVER_DOMAIN}/client/v1/accounts/register/email`; + const { data } = await axios.post(url, { + name: account.name, + email: account.email, + password: account.password, + }); + + const avatarPath = path.resolve(`src/seed/avatars/${account.avatar}`); + const avatarId = await uploadAvatar(data.token, avatarPath); + data.account.avatar = avatarId; + + const updateAccountUrl = `${SERVER_DOMAIN}/client/v1/accounts/${data.account.id}`; + await axios.put( + updateAccountUrl, + { + name: account.name, + avatar: avatarId, + }, + { + headers: { + Authorization: `Bearer ${data.token}`, + }, + } + ); + + return data; +}; + +const createMainAccountAndWorkspace = async ( + account: FakerAccount +): Promise => { + const login = await createAccount(account); + const workspace = login.workspaces[0]; + if (!workspace) { + throw new Error('Workspace not created.'); + } + + const avatarPath = path.resolve('src/seed/avatars/workspace_avatar.png'); + const avatarId = await uploadAvatar(login.token, avatarPath); + + workspace.name = 'Colanode'; + workspace.description = + 'This is a workspace for Colanode generated by the "seed" script'; + workspace.avatar = avatarId; + + // update workspace name and description here + const updateWorkspaceUrl = `${SERVER_DOMAIN}/client/v1/workspaces/${workspace.id}`; + await axios.put( + updateWorkspaceUrl, + { + name: workspace.name, + description: workspace.description, + avatar: avatarId, + }, + { + headers: { + Authorization: `Bearer ${login.token}`, + }, + } + ); + + return login; +}; + +const inviteAccountsToWorkspace = async ( + mainAccount: LoginOutput, + otherFakerAccounts: FakerAccount[] +) => { + const workspace = mainAccount.workspaces[0]; + if (!workspace) { + throw new Error('Workspace not found'); + } + + const url = `${SERVER_DOMAIN}/client/v1/workspaces/${workspace.id}/users`; + await axios.post( + url, + { + emails: otherFakerAccounts.map((account) => account.email), + role: 'admin', + }, + { + headers: { + Authorization: `Bearer ${mainAccount.token}`, + }, + } + ); +}; + +const sendTransactions = async (user: User, workspaceId: string) => { + const url = `${SERVER_DOMAIN}/client/v1/workspaces/${workspaceId}/transactions`; + const batchSize = 100; + const totalBatches = Math.ceil(user.transactions.length / batchSize); + let currentBatch = 1; + + // Create a copy of the transactions array to modify + const remainingTransactions = [...user.transactions]; + + while (remainingTransactions.length > 0) { + const batch = remainingTransactions.splice(0, batchSize); + + console.log( + `Sending batch ${currentBatch} of ${totalBatches} transactions for user ${user.login.account.email}` + ); + + await axios.post( + url, + { + transactions: batch, + }, + { + headers: { Authorization: `Bearer ${user.login.token}` }, + } + ); + + currentBatch++; + } +}; + +const seed = async () => { + const fakerAccountsJson = fs.readFileSync( + path.resolve('src/seed/accounts.json'), + 'utf8' + ); + const fakerAccounts: FakerAccount[] = JSON.parse(fakerAccountsJson); + const users: User[] = []; + + const mainFakkerAccount = fakerAccounts[0]; + if (!mainFakkerAccount) { + throw new Error('Main account not found'); + } + + console.log( + 'Creating main account and workspace', + mainFakkerAccount.name, + mainFakkerAccount.email + ); + const mainAccount = await createMainAccountAndWorkspace(mainFakkerAccount); + const workspace = mainAccount.workspaces[0]; + if (!workspace) { + throw new Error('Workspace not found'); + } + + users.push({ + login: mainAccount, + userId: workspace.user.id, + transactions: [], + }); + + const otherAccounts = fakerAccounts.slice(1); + console.log('Inviting other accounts to workspace'); + await inviteAccountsToWorkspace(mainAccount, otherAccounts); + + for (const fakerAccount of otherAccounts) { + console.log('Creating account', fakerAccount.name, fakerAccount.email); + const account = await createAccount(fakerAccount); + + const accountWorkspace = account.workspaces[0]; + if (!accountWorkspace) { + throw new Error('Workspace not found'); + } + + users.push({ + login: account, + userId: accountWorkspace.user.id, + transactions: [], + }); + } + + console.log('Generating nodes'); + const nodeGenerator = new NodeGenerator(workspace.id, users); + nodeGenerator.generate(); + + console.log('Sending transactions'); + for (const user of users) { + console.log('Sending transactions for user', user.login.account.email); + await sendTransactions(user, workspace.id); + } + + console.log('Done'); +}; + +seed(); diff --git a/scripts/src/seed/node-generator.ts b/scripts/src/seed/node-generator.ts new file mode 100644 index 00000000..84e7c1db --- /dev/null +++ b/scripts/src/seed/node-generator.ts @@ -0,0 +1,560 @@ +import { + Block, + generateId, + IdType, + LocalTransaction, + NodeAttributes, + NodeRole, + registry, + generateNodeIndex, + ViewAttributes, + FieldAttributes, + SelectOptionAttributes, + DatabaseAttributes, + FieldValue, + ViewFilterAttributes, +} from '@colanode/core'; +import { encodeState, YDoc } from '@colanode/crdt'; +import { faker } from '@faker-js/faker'; + +import { User } from './types'; + +const MESSAGES_PER_CONVERSATION = 500; +const RECORDS_PER_DATABASE = 1000; + +export class NodeGenerator { + constructor( + private readonly workspaceId: string, + private readonly users: User[] + ) {} + + public generate() { + this.buildGeneralSpace(); + this.buildProductSpace(); + this.buildChats(); + } + + private buildGeneralSpace() { + const spaceId = this.buildSpace('General', 'The general space'); + this.buildPage('Welcome', spaceId); + this.buildPage('Resources', spaceId); + this.buildPage('Guide', spaceId); + this.buildChannel('Announcements', spaceId); + } + + private buildProductSpace() { + const spaceId = this.buildSpace('Product', 'The product space'); + this.buildChannel('Discussions', spaceId); + this.buildChannel('Alerts', spaceId); + this.buildPage('Roadmap', spaceId); + this.buildTasksDatabase(spaceId); + } + + private buildChats() { + for (let i = 1; i < this.users.length; i++) { + const user = this.users[i]!; + this.buildChat(user); + } + } + + private buildSpace(name: string, description: string) { + const spaceId = generateId(IdType.Space); + const collaborators: Record = {}; + for (const user of this.users) { + collaborators[user.userId] = 'admin'; + } + + const spaceAttributes: NodeAttributes = { + type: 'space', + name, + description, + parentId: this.workspaceId, + collaborators, + }; + + const user = this.getMainUser(); + const createTransaction = this.buildCreateTransaction( + spaceId, + user.userId, + spaceAttributes + ); + + user.transactions.push(createTransaction); + return spaceId; + } + + private buildChannel(name: string, spaceId: string) { + const channelId = generateId(IdType.Channel); + const channelAttributes: NodeAttributes = { + type: 'channel', + name, + parentId: spaceId, + }; + + const user = this.getMainUser(); + const createTransaction = this.buildCreateTransaction( + channelId, + user.userId, + channelAttributes + ); + + user.transactions.push(createTransaction); + + this.buidMessages(channelId, MESSAGES_PER_CONVERSATION, this.users); + } + + private buildChat(user: User) { + const mainUser = this.getMainUser(); + const chatId = generateId(IdType.Chat); + const chatAttributes: NodeAttributes = { + type: 'chat', + parentId: this.workspaceId, + collaborators: { + [mainUser.userId]: 'admin', + [user.userId]: 'admin', + }, + }; + + const createTransaction = this.buildCreateTransaction( + chatId, + mainUser.userId, + chatAttributes + ); + + mainUser.transactions.push(createTransaction); + + this.buidMessages(chatId, MESSAGES_PER_CONVERSATION, [mainUser, user]); + } + + private buildPage(name: string, parentId: string) { + const pageId = generateId(IdType.Page); + const pageAttributes: NodeAttributes = { + type: 'page', + name, + parentId, + content: this.buildDocumentContent(pageId), + }; + + const user = this.getMainUser(); + const createTransaction = this.buildCreateTransaction( + pageId, + user.userId, + pageAttributes + ); + + user.transactions.push(createTransaction); + } + + private buidMessages(conversationId: string, count: number, users: User[]) { + for (let i = 0; i < count; i++) { + this.buildMessage(conversationId, users); + } + } + + private buildMessage(conversationId: string, users: User[]) { + const messageId = generateId(IdType.Message); + + const messageAttributes: NodeAttributes = { + type: 'message', + content: this.buildMessageContent(messageId), + parentId: conversationId, + subtype: 'standard', + reactions: {}, + }; + + const user = this.getRandomUser(users); + const createTransaction = this.buildCreateTransaction( + messageId, + user.userId, + messageAttributes + ); + + user.transactions.push(createTransaction); + } + + private buildTasksDatabase(parentId: string) { + const databaseId = generateId(IdType.Database); + + const newStatusOption: SelectOptionAttributes = { + id: generateId(IdType.SelectOption), + name: 'New', + color: 'gray', + index: generateNodeIndex(), + }; + + const activeStatusOption: SelectOptionAttributes = { + id: generateId(IdType.SelectOption), + name: 'Active', + color: 'blue', + index: generateNodeIndex(newStatusOption.index), + }; + + const toTestStatusOption: SelectOptionAttributes = { + id: generateId(IdType.SelectOption), + name: 'To Test', + color: 'yellow', + index: generateNodeIndex(activeStatusOption.index), + }; + + const closedStatusOption: SelectOptionAttributes = { + id: generateId(IdType.SelectOption), + name: 'Closed', + color: 'red', + index: generateNodeIndex(toTestStatusOption.index), + }; + + const statusField: FieldAttributes = { + id: generateId(IdType.Field), + type: 'select', + name: 'Status', + index: generateNodeIndex(), + options: { + [newStatusOption.id]: newStatusOption, + [activeStatusOption.id]: activeStatusOption, + [toTestStatusOption.id]: toTestStatusOption, + [closedStatusOption.id]: closedStatusOption, + }, + }; + + const apiTeamSelectOption: SelectOptionAttributes = { + id: generateId(IdType.SelectOption), + name: 'api', + color: 'blue', + index: generateNodeIndex(), + }; + + const devopsTeamSelectOption: SelectOptionAttributes = { + id: generateId(IdType.SelectOption), + name: 'devops', + color: 'green', + index: generateNodeIndex(apiTeamSelectOption.index), + }; + + const frontendTeamSelectOption: SelectOptionAttributes = { + id: generateId(IdType.SelectOption), + name: 'frontend', + color: 'purple', + index: generateNodeIndex(devopsTeamSelectOption.index), + }; + + const aiTeamSelectOption: SelectOptionAttributes = { + id: generateId(IdType.SelectOption), + name: 'ai', + color: 'pink', + index: generateNodeIndex(frontendTeamSelectOption.index), + }; + + const otherTeamSelectOption: SelectOptionAttributes = { + id: generateId(IdType.SelectOption), + name: 'other', + color: 'gray', + index: generateNodeIndex(aiTeamSelectOption.index), + }; + + const teamsField: FieldAttributes = { + id: generateId(IdType.Field), + type: 'multiSelect', + name: 'Teams', + index: generateNodeIndex(statusField.index), + options: { + [apiTeamSelectOption.id]: apiTeamSelectOption, + [devopsTeamSelectOption.id]: devopsTeamSelectOption, + [frontendTeamSelectOption.id]: frontendTeamSelectOption, + [aiTeamSelectOption.id]: aiTeamSelectOption, + [otherTeamSelectOption.id]: otherTeamSelectOption, + }, + }; + + const assignedField: FieldAttributes = { + id: generateId(IdType.Field), + type: 'collaborator', + name: 'Assigned', + index: generateNodeIndex(teamsField.index), + }; + + const priorityField: FieldAttributes = { + id: generateId(IdType.Field), + type: 'number', + name: 'Priority', + index: generateNodeIndex(assignedField.index), + }; + + const approvedField: FieldAttributes = { + id: generateId(IdType.Field), + type: 'boolean', + name: 'Approved', + index: generateNodeIndex(priorityField.index), + }; + + const releaseDateField: FieldAttributes = { + id: generateId(IdType.Field), + type: 'date', + name: 'Release Date', + index: generateNodeIndex(approvedField.index), + }; + + const commentsField: FieldAttributes = { + id: generateId(IdType.Field), + type: 'text', + name: 'Comment', + index: generateNodeIndex(releaseDateField.index), + }; + + const allTasksView: ViewAttributes = { + id: generateId(IdType.View), + type: 'table', + name: 'All Tasks', + avatar: null, + fields: {}, + filters: {}, + index: generateNodeIndex(), + nameWidth: null, + groupBy: null, + sorts: {}, + }; + + const activeTasksFilter: ViewFilterAttributes = { + id: generateId(IdType.ViewFilter), + type: 'field', + fieldId: statusField.id, + value: activeStatusOption.id, + operator: 'equals', + }; + + const activeTasksView: ViewAttributes = { + id: generateId(IdType.View), + type: 'table', + name: 'Active Tasks', + avatar: null, + fields: {}, + filters: { + [activeTasksFilter.id]: activeTasksFilter, + }, + index: generateNodeIndex(), + nameWidth: null, + groupBy: null, + sorts: {}, + }; + + const kanbanView: ViewAttributes = { + id: generateId(IdType.View), + type: 'board', + name: 'Kanban', + avatar: null, + fields: {}, + filters: {}, + index: generateNodeIndex(), + nameWidth: null, + groupBy: statusField.id, + sorts: {}, + }; + + const databaseAttributes: NodeAttributes = { + type: 'database', + parentId, + name: 'Tasks', + fields: { + [statusField.id]: statusField, + [teamsField.id]: teamsField, + [assignedField.id]: assignedField, + [priorityField.id]: priorityField, + [approvedField.id]: approvedField, + [releaseDateField.id]: releaseDateField, + [commentsField.id]: commentsField, + }, + views: { + [allTasksView.id]: allTasksView, + [activeTasksView.id]: activeTasksView, + [kanbanView.id]: kanbanView, + }, + }; + + const user = this.getMainUser(); + const createTransaction = this.buildCreateTransaction( + databaseId, + user.userId, + databaseAttributes + ); + + user.transactions.push(createTransaction); + + this.buildRecords(databaseId, databaseAttributes, RECORDS_PER_DATABASE); + } + + private buildRecords( + databaseId: string, + databaseAttributes: DatabaseAttributes, + count: number + ) { + for (let i = 0; i < count; i++) { + this.buildRecord(databaseId, databaseAttributes); + } + } + + private buildRecord( + databaseId: string, + databaseAttributes: DatabaseAttributes + ) { + const recordId = generateId(IdType.Record); + const recordAttributes: NodeAttributes = { + type: 'record', + parentId: databaseId, + databaseId, + content: this.buildDocumentContent(recordId), + name: faker.lorem.sentence(), + avatar: null, + fields: {}, + }; + + for (const field of Object.values(databaseAttributes.fields)) { + const fieldValue = this.buildFieldValue(field); + if (fieldValue) { + recordAttributes.fields[field.id] = fieldValue; + } + } + + const user = this.getRandomUser(this.users); + const createTransaction = this.buildCreateTransaction( + recordId, + user.userId, + recordAttributes + ); + + user.transactions.push(createTransaction); + } + + private getRandomUser(users: User[]): User { + const user = users[Math.floor(Math.random() * users.length)]; + if (!user) { + throw new Error('User not found'); + } + + return user; + } + + private getMainUser(): User { + return this.users[0]!; + } + + private buildCreateTransaction( + id: string, + userId: string, + attributes: NodeAttributes + ): LocalTransaction { + const ydoc = new YDoc(); + const model = registry.getModel(attributes.type); + + const update = ydoc.updateAttributes(model.schema, attributes); + + return { + id: generateId(IdType.Transaction), + operation: 'create', + data: encodeState(update), + nodeId: id, + nodeType: attributes.type, + createdAt: new Date().toISOString(), + createdBy: userId, + }; + } + + private buildMessageContent(messageId: string): Record { + const paragraphBlock = this.buildParagraphBlock( + messageId, + generateNodeIndex() + ); + return { + [paragraphBlock.id]: paragraphBlock, + }; + } + + private buildDocumentContent(pageId: string): Record { + const nrOfParagraphs = Math.floor(Math.random() * 10) + 1; + const blocks: Record = {}; + for (let i = 0; i < nrOfParagraphs; i++) { + const block = this.buildParagraphBlock(pageId, generateNodeIndex()); + blocks[block.id] = block; + } + + return blocks; + } + + private buildParagraphBlock(parentId: string, index: string): Block { + const blockId = generateId(IdType.Block); + return { + type: 'paragraph', + parentId, + content: [{ type: 'text', text: faker.lorem.sentence(), marks: null }], + id: blockId, + index, + attrs: null, + }; + } + + private buildFieldValue(field: FieldAttributes): FieldValue | null { + if (field.type === 'boolean') { + return { + type: 'boolean', + value: faker.datatype.boolean(), + }; + } else if (field.type === 'collaborator') { + return { + type: 'collaborator', + value: [this.getRandomUser(this.users).userId], + }; + } else if (field.type === 'date') { + return { + type: 'date', + value: faker.date.recent().toISOString(), + }; + } else if (field.type === 'email') { + return { + type: 'email', + value: faker.internet.email(), + }; + } else if (field.type === 'multiSelect') { + const options = Object.values(field.options ?? {}); + const randomOption = options[Math.floor(Math.random() * options.length)]; + if (!randomOption) { + return null; + } + + return { + type: 'multiSelect', + value: [randomOption.id], + }; + } else if (field.type === 'number') { + return { + type: 'number', + value: Math.floor(Math.random() * 1000), + }; + } else if (field.type === 'phone') { + return { + type: 'phone', + value: faker.phone.number(), + }; + } else if (field.type === 'select') { + const options = Object.values(field.options ?? {}); + const randomOption = options[Math.floor(Math.random() * options.length)]; + if (!randomOption) { + return null; + } + + return { + type: 'select', + value: randomOption.id, + }; + } else if (field.type === 'text') { + return { + type: 'text', + value: faker.lorem.sentence(), + }; + } else if (field.type === 'url') { + return { + type: 'url', + value: faker.internet.url(), + }; + } + + return null; + } +} diff --git a/scripts/src/seed/types.ts b/scripts/src/seed/types.ts new file mode 100644 index 00000000..56ae1235 --- /dev/null +++ b/scripts/src/seed/types.ts @@ -0,0 +1,14 @@ +import { LocalTransaction, LoginOutput } from '@colanode/core'; + +export type FakerAccount = { + name: string; + email: string; + password: string; + avatar: string; +}; + +export type User = { + login: LoginOutput; + userId: string; + transactions: LocalTransaction[]; +}; diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json index 9ed484a0..f1138060 100644 --- a/scripts/tsconfig.json +++ b/scripts/tsconfig.json @@ -4,5 +4,10 @@ "extends": "../tsconfig.base.json", "compilerOptions": { "resolveJsonModule": true - } + }, + "references": [ + { "path": "../packages/core/tsconfig.json" }, + { "path": "../packages/crdt/tsconfig.json" } + ], + "include": ["src"] }