Merge remote-tracking branch 'origin/develop' into staging

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2024-12-26 22:02:13 +07:00
38 changed files with 186 additions and 1527 deletions
-109
View File
@@ -164,15 +164,6 @@ dependencies:
'@rush-temp/cloud-transactor':
specifier: file:./projects/cloud-transactor.tgz
version: file:projects/cloud-transactor.tgz(@types/node@20.11.19)(bufferutil@4.0.8)(ts-node@10.9.2)(utf-8-validate@6.0.4)
'@rush-temp/cloud-transactor-api':
specifier: file:./projects/cloud-transactor-api.tgz
version: file:projects/cloud-transactor-api.tgz(esbuild@0.24.2)(ts-node@10.9.2)
'@rush-temp/cloud-transactor-api-demo':
specifier: file:./projects/cloud-transactor-api-demo.tgz
version: file:projects/cloud-transactor-api-demo.tgz(@types/node@20.11.19)(bufferutil@4.0.8)(esbuild@0.24.2)(ts-node@10.9.2)(utf-8-validate@6.0.4)
'@rush-temp/cloud-transactor-http-api':
specifier: file:./projects/cloud-transactor-http-api.tgz
version: file:projects/cloud-transactor-http-api.tgz(bufferutil@4.0.8)(esbuild@0.24.2)(ts-node@10.9.2)(utf-8-validate@6.0.4)
'@rush-temp/collaboration':
specifier: file:./projects/collaboration.tgz
version: file:projects/collaboration.tgz(esbuild@0.24.2)(ts-node@10.9.2)
@@ -23038,106 +23029,6 @@ packages:
- utf-8-validate
dev: false
file:projects/cloud-transactor-api-demo.tgz(@types/node@20.11.19)(bufferutil@4.0.8)(esbuild@0.24.2)(ts-node@10.9.2)(utf-8-validate@6.0.4):
resolution: {integrity: sha512-jDFW5bSJFL4PuJt0ESJ50jyMHtQKSd9eLfjoUXslmIUdAEITbEEz8N0cstMBujEybfmSghSi6haE2GnG/p4rHg==, tarball: file:projects/cloud-transactor-api-demo.tgz}
id: file:projects/cloud-transactor-api-demo.tgz
name: '@rush-temp/cloud-transactor-api-demo'
version: 0.0.0
dependencies:
'@cloudflare/workers-types': 4.20241022.0
'@types/jest': 29.5.12
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.6.2)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.6.2)
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.6.2)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
itty-router: 5.0.18
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
prettier: 3.2.5
ts-jest: 29.1.2(esbuild@0.24.2)(jest@29.7.0)(typescript@5.6.2)
typescript: 5.6.2
wrangler: 3.97.0(@cloudflare/workers-types@4.20241022.0)(bufferutil@4.0.8)(utf-8-validate@6.0.4)
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- '@types/node'
- babel-jest
- babel-plugin-macros
- bufferutil
- esbuild
- node-notifier
- supports-color
- ts-node
- utf-8-validate
dev: false
file:projects/cloud-transactor-api.tgz(esbuild@0.24.2)(ts-node@10.9.2):
resolution: {integrity: sha512-KdCN6yGHiBNfGsSWtSwQuIfOQGGr0P40cj5u8Z5p5AQz0MbeoRJ1gFnpzI8VJjOhKfNKn3JR8Zdwg+JTq29yWQ==, tarball: file:projects/cloud-transactor-api.tgz}
id: file:projects/cloud-transactor-api.tgz
name: '@rush-temp/cloud-transactor-api'
version: 0.0.0
dependencies:
'@types/jest': 29.5.12
'@types/node': 20.11.19
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.6.2)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.6.2)
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.6.2)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
prettier: 3.2.5
ts-jest: 29.1.2(esbuild@0.24.2)(jest@29.7.0)(typescript@5.6.2)
typescript: 5.6.2
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- babel-jest
- babel-plugin-macros
- esbuild
- node-notifier
- supports-color
- ts-node
dev: false
file:projects/cloud-transactor-http-api.tgz(bufferutil@4.0.8)(esbuild@0.24.2)(ts-node@10.9.2)(utf-8-validate@6.0.4):
resolution: {integrity: sha512-4vV2jdYPYycGKl0jomdYmnEvZgancPc2pdhJ6khPP3qR4FZFhHx5lF/uLuREoZ3d7GuQqNs4J+fKL/+iriCkaA==, tarball: file:projects/cloud-transactor-http-api.tgz}
id: file:projects/cloud-transactor-http-api.tgz
name: '@rush-temp/cloud-transactor-http-api'
version: 0.0.0
dependencies:
'@cloudflare/workers-types': 4.20241022.0
'@types/jest': 29.5.12
'@types/node': 20.11.19
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.6.2)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.6.2)
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.6.2)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
itty-router: 5.0.18
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
prettier: 3.2.5
ts-jest: 29.1.2(esbuild@0.24.2)(jest@29.7.0)(typescript@5.6.2)
typescript: 5.6.2
wrangler: 3.97.0(@cloudflare/workers-types@4.20241022.0)(bufferutil@4.0.8)(utf-8-validate@6.0.4)
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- babel-jest
- babel-plugin-macros
- bufferutil
- esbuild
- node-notifier
- supports-color
- ts-node
- utf-8-validate
dev: false
file:projects/cloud-transactor.tgz(@types/node@20.11.19)(bufferutil@4.0.8)(ts-node@10.9.2)(utf-8-validate@6.0.4):
resolution: {integrity: sha512-6kBv/3KfTsOvsDrfVXQvQwtLM72ltx4oPHc3cJ9xtG82XLeQLLpeJmCy6CNDVOtXaqnagwF5TKm/zCw4sc/dFQ==, tarball: file:projects/cloud-transactor.tgz}
id: file:projects/cloud-transactor.tgz
+17
View File
@@ -21,6 +21,7 @@ import core, {
TxProcessor,
getCurrentAccount,
reduceCalls,
type Account,
type AnyAttribute,
type ArrOf,
type AttachedDoc,
@@ -253,6 +254,18 @@ export function getClient (): TxOperations & Client {
return clientProxy
}
export type OnClientListener = (client: Client, account: Account) => void
const onClientListeners: OnClientListener[] = []
export function onClient (l: OnClientListener): void {
onClientListeners.push(l)
if (client !== undefined) {
setTimeout(() => {
l(client, getCurrentAccount())
})
}
}
let txQueue: Tx[] = []
export type RefreshListener = () => void
@@ -306,6 +319,10 @@ export async function setClient (_client: Client): Promise<void> {
if (needRefresh || globalQueries.length > 0) {
await refreshClient(true)
}
const acc = getCurrentAccount()
onClientListeners.forEach((l) => {
l(_client, acc)
})
}
/**
* @public
+11 -21
View File
@@ -14,31 +14,21 @@
//
import activity, { type ActivityMessage, type SavedMessage } from '@hcengineering/activity'
import core, { type Ref, SortingOrder, type WithLookup } from '@hcengineering/core'
import { createQuery, onClient } from '@hcengineering/presentation'
import { writable } from 'svelte/store'
import { createQuery, getClient } from '@hcengineering/presentation'
export const savedMessagesStore = writable<Array<WithLookup<SavedMessage>>>([])
export const messageInFocus = writable<Ref<ActivityMessage> | undefined>(undefined)
const savedMessagesQuery = createQuery(true)
export function loadSavedMessages (): void {
const client = getClient()
if (client !== undefined) {
savedMessagesQuery.query(
activity.class.SavedMessage,
{ space: core.space.Workspace },
(res) => {
savedMessagesStore.set(res.filter(({ $lookup }) => $lookup?.attachedTo !== undefined))
},
{ lookup: { attachedTo: activity.class.ActivityMessage }, sort: { modifiedOn: SortingOrder.Descending } }
)
} else {
setTimeout(() => {
loadSavedMessages()
}, 50)
}
}
loadSavedMessages()
onClient(() => {
savedMessagesQuery.query(
activity.class.SavedMessage,
{ space: core.space.Workspace },
(res) => {
savedMessagesStore.set(res.filter(({ $lookup }) => $lookup?.attachedTo !== undefined))
},
{ lookup: { attachedTo: activity.class.ActivityMessage }, sort: { modifiedOn: SortingOrder.Descending } }
)
})
+12 -22
View File
@@ -5,12 +5,12 @@ import {
type ReccuringInstance,
generateEventId
} from '@hcengineering/calendar'
import { type IdMap, type Timestamp, getCurrentAccount, toIdMap, type DocumentUpdate } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { showPopup, closePopup, DAY } from '@hcengineering/ui'
import { type DocumentUpdate, type IdMap, type Timestamp, getCurrentAccount, toIdMap } from '@hcengineering/core'
import { createQuery, getClient, onClient } from '@hcengineering/presentation'
import { closePopup, DAY, showPopup } from '@hcengineering/ui'
import { writable } from 'svelte/store'
import calendar from './plugin'
import UpdateRecInstancePopup from './components/UpdateRecInstancePopup.svelte'
import calendar from './plugin'
export function saveUTC (date: Timestamp): Timestamp {
const utcdate = new Date(date)
@@ -74,24 +74,14 @@ export const calendarByIdStore = writable<IdMap<Calendar>>(new Map())
export const calendarStore = writable<Calendar[]>([])
export const visibleCalendarStore = writable<Calendar[]>([])
function fillStores (): void {
const client = getClient()
if (client !== undefined) {
const query = createQuery(true)
query.query(calendar.class.Calendar, {}, (res) => {
calendarStore.set(res)
visibleCalendarStore.set(res.filter((p) => !p.hidden))
calendarByIdStore.set(toIdMap(res))
})
} else {
setTimeout(() => {
fillStores()
}, 50)
}
}
fillStores()
const query = createQuery(true)
onClient((client, account) => {
query.query(calendar.class.Calendar, {}, (res) => {
calendarStore.set(res)
visibleCalendarStore.set(res.filter((p) => !p.hidden))
calendarByIdStore.set(toIdMap(res))
})
})
export async function updatePast (ops: DocumentUpdate<Event>, object: ReccuringInstance): Promise<void> {
const client = getClient()
@@ -28,7 +28,7 @@ import core, {
} from '@hcengineering/core'
import notification, { type DocNotifyContext } from '@hcengineering/notification'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { createQuery, getClient, MessageBox } from '@hcengineering/presentation'
import { createQuery, getClient, MessageBox, onClient } from '@hcengineering/presentation'
import { type Action, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view'
import workbench, { type SpecialNavModel } from '@hcengineering/workbench'
@@ -364,12 +364,9 @@ function archiveActivityChannels (contexts: DocNotifyContext[]): void {
)
}
const savedAttachmentsQuery = createQuery(true)
export function loadSavedAttachments (): void {
const client = getClient()
if (client !== undefined) {
const savedAttachmentsQuery = createQuery(true)
onClient(() => {
savedAttachmentsQuery.query(
attachment.class.SavedAttachments,
{ space: core.space.Workspace },
@@ -378,11 +375,7 @@ export function loadSavedAttachments (): void {
},
{ lookup: { attachedTo: attachment.class.Attachment }, sort: { modifiedOn: SortingOrder.Descending } }
)
} else {
setTimeout(() => {
loadSavedAttachments()
}, 50)
}
})
}
export async function hideActivityChannels (contexts: DocNotifyContext[]): Promise<void> {
+23 -33
View File
@@ -52,7 +52,7 @@ import core, {
} from '@hcengineering/core'
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
import { type IntlString, getEmbeddedLabel, getResource, translate } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { createQuery, getClient, onClient } from '@hcengineering/presentation'
import { type TemplateDataProvider } from '@hcengineering/templates'
import {
getCurrentResolvedLocation,
@@ -323,42 +323,32 @@ export const personByIdStore = derived([personAccountPersonByIdStore, employeeBy
return new Map([...m1, ...m2])
})
function fillStores (): void {
const client = getClient()
const query = createQuery(true)
const accountQ = createQuery(true)
const accountPersonQuery = createQuery(true)
const providerQuery = createQuery(true)
if (client !== undefined) {
const accountPersonQuery = createQuery(true)
onClient(() => {
query.query(contact.mixin.Employee, { active: { $in: [true, false] } }, (res) => {
employeesStore.set(res)
employeeByIdStore.set(toIdMap(res))
})
const query = createQuery(true)
query.query(contact.mixin.Employee, { active: { $in: [true, false] } }, (res) => {
employeesStore.set(res)
employeeByIdStore.set(toIdMap(res))
accountQ.query(contact.class.PersonAccount, {}, (res) => {
personAccountByIdStore.set(toIdMap(res))
const persons = res.map((it) => it.person)
accountPersonQuery.query<Person>(contact.class.Person, { _id: { $in: persons } }, (res) => {
const personIn = toIdMap(res)
personAccountPersonByIdStore.set(personIn)
})
})
const accountQ = createQuery(true)
accountQ.query(contact.class.PersonAccount, {}, (res) => {
personAccountByIdStore.set(toIdMap(res))
const persons = res.map((it) => it.person)
accountPersonQuery.query<Person>(contact.class.Person, { _id: { $in: persons } }, (res) => {
const personIn = toIdMap(res)
personAccountPersonByIdStore.set(personIn)
})
})
const providerQuery = createQuery(true)
providerQuery.query(contact.class.ChannelProvider, {}, (res) => {
channelProviders.set(res)
})
} else {
setTimeout(() => {
fillStores()
}, 50)
}
}
fillStores()
providerQuery.query(contact.class.ChannelProvider, {}, (res) => {
channelProviders.set(res)
})
})
const userStatusesQuery = createQuery(true)
+64 -72
View File
@@ -11,7 +11,7 @@ import {
type Room,
type MeetingMinutes
} from '@hcengineering/love'
import { createQuery, getClient } from '@hcengineering/presentation'
import { createQuery, onClient } from '@hcengineering/presentation'
import { derived, get, writable } from 'svelte/store'
import { personIdByAccountId } from '@hcengineering/contact-resources'
import aiBot from '@hcengineering/ai-bot'
@@ -79,77 +79,69 @@ function filterParticipantInfo (value: ParticipantInfo[]): ParticipantInfo[] {
export const storePromise = writable<Promise<void>>(new Promise((resolve) => {}))
function fillStores (): void {
const client = getClient()
if (client !== undefined || getCurrentAccount() != null) {
const query = createQuery(true)
const roomPromise = new Promise<void>((resolve) =>
query.query(love.class.Room, {}, (res) => {
rooms.set(res)
resolve()
})
)
const statusQuery = createQuery(true)
const infoPromise = new Promise<void>((resolve) =>
statusQuery.query(love.class.ParticipantInfo, {}, (res) => {
infos.set(filterParticipantInfo(res))
resolve()
})
)
const floorsQuery = createQuery(true)
const floorPromise = new Promise<void>((resolve) =>
floorsQuery.query(love.class.Floor, {}, (res) => {
floors.set(res)
resolve()
})
)
const requestsQuery = createQuery(true)
const requestPromise = new Promise<void>((resolve) =>
requestsQuery.query(
love.class.JoinRequest,
{ person: (getCurrentAccount() as PersonAccount).person, status: RequestStatus.Pending },
(res) => {
myRequests.set(res)
resolve()
}
)
)
const preferencesQuery = createQuery(true)
const preferencePromise = new Promise<void>((resolve) =>
preferencesQuery.query(love.class.DevicesPreference, {}, (res) => {
myPreferences.set(res[0])
$myPreferences = res[0]
resolve()
})
)
const invitesQuery = createQuery(true)
const invitesPromise = new Promise<void>((resolve) =>
invitesQuery.query(love.class.Invite, { status: RequestStatus.Pending }, (res) => {
invites.set(res)
resolve()
})
)
storePromise.set(
new Promise((resolve) => {
void Promise.all([
roomPromise,
infoPromise,
floorPromise,
requestPromise,
preferencePromise,
invitesPromise
]).then(() => {
resolve()
})
})
)
} else {
setTimeout(() => {
fillStores()
}, 50)
}
}
const query = createQuery(true)
const statusQuery = createQuery(true)
const floorsQuery = createQuery(true)
const requestsQuery = createQuery(true)
const preferencesQuery = createQuery(true)
const invitesQuery = createQuery(true)
fillStores()
onClient(() => {
const roomPromise = new Promise<void>((resolve) =>
query.query(love.class.Room, {}, (res) => {
rooms.set(res)
resolve()
})
)
const infoPromise = new Promise<void>((resolve) =>
statusQuery.query(love.class.ParticipantInfo, {}, (res) => {
infos.set(filterParticipantInfo(res))
resolve()
})
)
const floorPromise = new Promise<void>((resolve) =>
floorsQuery.query(love.class.Floor, {}, (res) => {
floors.set(res)
resolve()
})
)
const requestPromise = new Promise<void>((resolve) =>
requestsQuery.query(
love.class.JoinRequest,
{ person: (getCurrentAccount() as PersonAccount).person, status: RequestStatus.Pending },
(res) => {
myRequests.set(res)
resolve()
}
)
)
const preferencePromise = new Promise<void>((resolve) =>
preferencesQuery.query(love.class.DevicesPreference, {}, (res) => {
myPreferences.set(res[0])
$myPreferences = res[0]
resolve()
})
)
const invitesPromise = new Promise<void>((resolve) =>
invitesQuery.query(love.class.Invite, { status: RequestStatus.Pending }, (res) => {
invites.set(res)
resolve()
})
)
storePromise.set(
new Promise((resolve) => {
void Promise.all([
roomPromise,
infoPromise,
floorPromise,
requestPromise,
preferencePromise,
invitesPromise
]).then(() => {
resolve()
})
})
)
})
export const lockedRoom = writable<string>('')
+23 -33
View File
@@ -14,8 +14,10 @@
// limitations under the License.
//
import activity from '@hcengineering/activity'
import chunter from '@hcengineering/chunter'
import { type Employee, type PersonAccount } from '@hcengineering/contact'
import core, {
getCurrentAccount,
toIdMap,
type Attribute,
type Class,
@@ -27,7 +29,7 @@ import core, {
type TxOperations
} from '@hcengineering/core'
import { type IntlString, type Resources } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { createQuery, getClient, onClient } from '@hcengineering/presentation'
import task, {
getStatusIndex,
makeRank,
@@ -41,9 +43,6 @@ import { getCurrentLocation, navigate, showPopup } from '@hcengineering/ui'
import { type ViewletDescriptor } from '@hcengineering/view'
import { CategoryQuery, groupBy, statusStore } from '@hcengineering/view-resources'
import { get, writable } from 'svelte/store'
import { type Employee, type PersonAccount } from '@hcengineering/contact'
import activity from '@hcengineering/activity'
import chunter from '@hcengineering/chunter'
import AssignedTasks from './components/AssignedTasks.svelte'
import Dashboard from './components/Dashboard.svelte'
@@ -69,14 +68,14 @@ import TaskKindSelector from './components/taskTypes/TaskKindSelector.svelte'
import TaskTypeClassPresenter from './components/taskTypes/TaskTypeClassPresenter.svelte'
import TaskTypePresenter from './components/taskTypes/TaskTypePresenter.svelte'
import ProjectTypeSelector from './components/projectTypes/ProjectTypeSelector.svelte'
import { employeeByIdStore, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
import CreateProjectType from './components/projectTypes/CreateProjectType.svelte'
import ProjectTypeGeneralSectionEditor from './components/projectTypes/ProjectTypeGeneralSectionEditor.svelte'
import ProjectTypeTasksTypeSectionEditor from './components/projectTypes/ProjectTypeTasksTypeSectionEditor.svelte'
import ProjectTypeAutomationsSectionEditor from './components/projectTypes/ProjectTypeAutomationsSectionEditor.svelte'
import ProjectTypeCollectionsSectionEditor from './components/projectTypes/ProjectTypeCollectionsSectionEditor.svelte'
import ProjectTypeGeneralSectionEditor from './components/projectTypes/ProjectTypeGeneralSectionEditor.svelte'
import ProjectTypeSelector from './components/projectTypes/ProjectTypeSelector.svelte'
import ProjectTypeTasksTypeSectionEditor from './components/projectTypes/ProjectTypeTasksTypeSectionEditor.svelte'
import TaskTypeEditor from './components/taskTypes/TaskTypeEditor.svelte'
import { employeeByIdStore, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
export { default as AssigneePresenter } from './components/AssigneePresenter.svelte'
export { default as TypeSelector } from './components/TypeSelector.svelte'
@@ -364,33 +363,24 @@ export const taskTypeStore = writable<IdMap<TaskType>>(new Map())
export const typesOfJoinedProjectsStore = writable<Array<Ref<ProjectType>>>()
export const joinedProjectsStore = writable<Project[]>()
function fillStores (): void {
const client = getClient()
const query = createQuery(true)
const taskQuery = createQuery(true)
const projectQuery = createQuery(true)
if (client !== undefined && getCurrentAccount() != null) {
const query = createQuery(true)
query.query(task.class.ProjectType, {}, (res) => {
typeStore.set(toIdMap(res))
})
onClient((client, user) => {
query.query(task.class.ProjectType, {}, (res) => {
typeStore.set(toIdMap(res))
})
const taskQuery = createQuery(true)
taskQuery.query(task.class.TaskType, {}, (res) => {
taskTypeStore.set(toIdMap(res))
})
taskQuery.query(task.class.TaskType, {}, (res) => {
taskTypeStore.set(toIdMap(res))
})
const projectQuery = createQuery(true)
projectQuery.query(task.class.Project, { members: getCurrentAccount()._id }, (res) => {
typesOfJoinedProjectsStore.set(res.map((r) => r.type).filter((it, idx, arr) => arr.indexOf(it) === idx))
joinedProjectsStore.set(res)
})
} else {
setTimeout(() => {
fillStores()
}, 50)
}
}
fillStores()
projectQuery.query(task.class.Project, { members: user._id }, (res) => {
typesOfJoinedProjectsStore.set(res.map((r) => r.type).filter((it, idx, arr) => arr.indexOf(it) === idx))
joinedProjectsStore.set(res)
})
})
export const selectedTypeStore = writable<Ref<ProjectType> | undefined>(undefined)
export const selectedTaskTypeStore = writable<Ref<TaskType> | undefined>(undefined)
+21 -32
View File
@@ -36,7 +36,7 @@ import core, {
type TxUpdateDoc
} from '@hcengineering/core'
import { type IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { createQuery, getClient, onClient } from '@hcengineering/presentation'
import task, { getStatusIndex, makeRank, type ProjectType } from '@hcengineering/task'
import { activeProjects as taskActiveProjects, taskTypeStore } from '@hcengineering/task-resources'
import {
@@ -577,36 +577,25 @@ export interface IssueRef {
export type IssueReverseRevMap = Map<Ref<Doc>, IssueRef[]>
export const relatedIssues = writable<IssueReverseRevMap>(new Map())
function fillStores (): void {
const client = getClient()
if (client !== undefined) {
const relatedIssuesQuery = createQuery(true)
relatedIssuesQuery.query(
tracker.class.Issue,
{ 'relations._id': { $exists: true } },
(res) => {
const nMap: IssueReverseRevMap = new Map()
for (const r of res) {
for (const rr of r.relations ?? []) {
nMap.set(rr._id, [...(nMap.get(rr._id) ?? []), { _id: r._id, status: r.status }])
}
}
relatedIssues.set(nMap)
},
{
projection: {
relations: 1,
status: 1
const relatedIssuesQuery = createQuery(true)
onClient(() => {
relatedIssuesQuery.query(
tracker.class.Issue,
{ 'relations._id': { $exists: true } },
(res) => {
const nMap: IssueReverseRevMap = new Map()
for (const r of res) {
for (const rr of r.relations ?? []) {
nMap.set(rr._id, [...(nMap.get(rr._id) ?? []), { _id: r._id, status: r.status }])
}
}
)
} else {
setTimeout(() => {
fillStores()
}, 50)
}
}
fillStores()
relatedIssues.set(nMap)
},
{
projection: {
relations: 1,
status: 1
}
}
)
})
+11 -21
View File
@@ -14,7 +14,7 @@
//
import core, { type IdMap, type Status, toIdMap } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { createQuery, onClient } from '@hcengineering/presentation'
import { writable } from 'svelte/store'
interface Store {
@@ -28,23 +28,13 @@ export const statusStore = writable<Store>({
array: []
})
function fillStores (): void {
const client = getClient()
if (client !== undefined) {
const query = createQuery(true)
query.query(core.class.Status, {}, (res) => {
const obj = {
byId: toIdMap(res),
array: res
}
statusStore.set(obj)
})
} else {
setTimeout(() => {
fillStores()
}, 50)
}
}
fillStores()
const query = createQuery(true)
onClient(() => {
query.query(core.class.Status, {}, (res) => {
const obj = {
byId: toIdMap(res),
array: res
}
statusStore.set(obj)
})
})
-15
View File
@@ -2230,21 +2230,6 @@
"packageName": "@hcengineering/cloud-transactor",
"projectFolder": "workers/transactor",
"shouldPublish": false
},
{
"packageName": "@hcengineering/cloud-transactor-api",
"projectFolder": "workers/transactor-api",
"shouldPublish": false
},
{
"packageName": "@hcengineering/cloud-transactor-api-demo",
"projectFolder": "workers/transactor-api-demo",
"shouldPublish": false
},
{
"packageName": "@hcengineering/cloud-transactor-http-api",
"projectFolder": "workers/transactor-http-api",
"shouldPublish": false
}
]
}
-7
View File
@@ -1,7 +0,0 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
@@ -1,4 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig"
}
@@ -1,7 +0,0 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
-45
View File
@@ -1,45 +0,0 @@
{
"name": "@hcengineering/cloud-transactor-api-demo",
"version": "0.6.0",
"main": "lib/index.js",
"types": "types/index.d.ts",
"template": "cloud",
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev --port 4050",
"dev-local": "wrangler dev --port 4050 --local --upstream-protocol=http",
"start": "wrangler dev --port 4050",
"cf-typegen": "wrangler types",
"build": "compile",
"build:watch": "compile",
"test": "jest --passWithNoTests --silent --forceExit",
"format": "format src",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent --forceExit",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20241022.0",
"@hcengineering/platform-rig": "^0.6.0",
"@types/jest": "^29.5.5",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"jest": "^29.7.0",
"prettier": "^3.1.0",
"ts-jest": "^29.1.1",
"typescript": "^5.3.3",
"wrangler": "^3.97.0"
},
"dependencies": {
"@hcengineering/core": "^0.6.32",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/cloud-transactor-api": "^0.6.0",
"itty-router": "^5.0.18"
}
}
-166
View File
@@ -1,166 +0,0 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
type ConnectOptions,
type TransactorRawApi,
type TransactorService,
createHttpClient,
createRpcClient,
getWorkspaceLogin,
unpackModel
} from '@hcengineering/cloud-transactor-api'
import contact, { AvatarType, type Person } from '@hcengineering/contact'
import core, { type AccountClient, type Ref, type TxCreateDoc, TxOperations, generateId } from '@hcengineering/core'
import { Router, error, json } from 'itty-router'
async function callClient<T> (client: T, method: () => Promise<any>): Promise<Response> {
try {
return json(await method())
} catch (error) {
console.error({ error })
throw error
} finally {
if (Symbol.dispose in (client as any)) {
;(client as any)[Symbol.dispose]()
}
}
}
export default {
async fetch (request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const transactorService = env.TRANSACTOR_SERVICE as any as TransactorService
function getConnectOpts (params: Record<string, any>): ConnectOptions {
return {
authOptions: {
email: params.email,
password: params.password,
workspace: params.workspace
},
serverConfig: {
ACCOUNTS_URL: env.ACCOUNTS_URL
},
workspaceId: params.workspace,
loadModel: params.loadModel === true
}
}
async function rawClient (params: Record<string, any>): Promise<TransactorRawApi> {
const email = params.email
const password = params.password
const workspace = params.workspace
const info = await getWorkspaceLogin('', { email, password, workspace }, { ACCOUNTS_URL: env.ACCOUNTS_URL })
return await transactorService.openRpc(info.token, params.workspace)
}
async function rpcClient (params: Record<string, any>): Promise<AccountClient> {
return await createRpcClient(transactorService, getConnectOpts(params))
}
async function httpClient (params: Record<string, any>): Promise<AccountClient> {
return await createHttpClient(env.HTTP_API_URL, getConnectOpts(params))
}
const router = Router()
router
.get('/demo-find-raw/:email/:password/:workspace', async ({ params }) => {
const client = await rawClient(params)
return await callClient(client, async () => {
return await client.findAll(contact.class.Person, {})
})
})
.get('/demo-find-rpc/:email/:password/:workspace', async ({ params }) => {
const client = await rpcClient(params)
return await callClient(client, async () => {
return await client.findAll(contact.class.Person, {})
})
})
.get('/demo-find-http/:email/:password/:workspace', async ({ params }) => {
const client = await httpClient(params)
return await callClient(client, async () => {
return await client.findAll(contact.class.Person, {})
})
})
.get('/demo-get-model-raw/:email/:password/:workspace', async ({ params }) => {
const client = await rawClient(params)
return await callClient(client, async () => {
return await unpackModel(await client.getModel())
})
})
.get('/demo-get-model-rpc/:email/:password/:workspace', async ({ params }) => {
const client = await rpcClient({ ...params, loadModel: true })
return await callClient(client, async () => {
return client.getModel()
})
})
.get('/demo-get-model-http/:email/:password/:workspace', async ({ params }) => {
const client = await httpClient({ ...params, loadModel: true })
return await callClient(client, async () => {
return client.getModel()
})
})
.get('/demo-tx-raw/:email/:password/:workspace', async ({ params }) => {
const client = await rawClient(params)
return await callClient(client, async () => {
const account = await client.getAccount()
const id = generateId()
const tx: TxCreateDoc<Person> = {
_id: id as Ref<TxCreateDoc<Person>>,
_class: core.class.TxCreateDoc,
space: core.space.Tx,
objectId: id as Ref<Person>,
objectClass: contact.class.Person,
objectSpace: account.space,
modifiedOn: Date.now(),
modifiedBy: account._id,
createdBy: account._id,
attributes: {
name: 'Person ' + id,
city: 'Unknown',
avatarType: AvatarType.COLOR
}
}
return await client.tx(tx)
})
})
.get('/demo-tx-rpc/:email/:password/:workspace', async ({ params }) => {
const client = await rpcClient({ ...params, loadModel: true })
return await callClient(client, async () => {
const account = await client.getAccount()
const txops = new TxOperations(client, account._id)
return await txops.createDoc(contact.class.Person, account.space, {
name: 'Person ' + generateId(),
city: 'Unknown',
avatarType: AvatarType.COLOR
})
})
})
.get('/demo-tx-http/:email/:password/:workspace', async ({ params }) => {
const client = await httpClient({ ...params, loadModel: true })
return await callClient(client, async () => {
const account = await client.getAccount()
const txops = new TxOperations(client, account._id)
return await txops.createDoc(contact.class.Person, account.space, {
name: 'Person ' + generateId(),
city: 'Unknown',
avatarType: AvatarType.COLOR
})
})
})
.all('*', () => error(404))
return await router.fetch(request).catch(error)
}
} satisfies ExportedHandler<Env>
-11
View File
@@ -1,11 +0,0 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo",
"types": ["@cloudflare/workers-types", "jest"]
}
}
-7
View File
@@ -1,7 +0,0 @@
// Generated by Wrangler by running `wrangler types`
interface Env {
ACCOUNTS_URL: "http://127.0.0.1:3000";
HTTP_API_URL: "http://127.0.0.1:4040";
TRANSACTOR_SERVICE: Fetcher;
}
-15
View File
@@ -1,15 +0,0 @@
#:schema node_modules/wrangler/config-schema.json
name = "cloud-transactor-api-demo"
main = "src/index.ts"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
keep_vars = true
[vars]
ACCOUNTS_URL = "http://127.0.0.1:3000"
HTTP_API_URL = "http://127.0.0.1:4040"
[[services]]
binding = "TRANSACTOR_SERVICE"
service = "cloud-transactor"
entrypoint = "TransactorRpc"
-7
View File
@@ -1,7 +0,0 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
-5
View File
@@ -1,5 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "node"
}
-7
View File
@@ -1,7 +0,0 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
-36
View File
@@ -1,36 +0,0 @@
{
"name": "@hcengineering/cloud-transactor-api",
"version": "0.6.0",
"main": "lib/index.js",
"types": "types/index.d.ts",
"template": "node",
"scripts": {
"build": "compile",
"build:watch": "compile",
"test": "jest --passWithNoTests --silent --forceExit",
"format": "format src",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent --forceExit",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@types/jest": "^29.5.5",
"@types/node": "~20.11.16",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"jest": "^29.7.0",
"prettier": "^3.1.0",
"ts-jest": "^29.1.1",
"typescript": "^5.3.3"
},
"dependencies": {
"@hcengineering/core": "^0.6.32"
}
}
-137
View File
@@ -1,137 +0,0 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { concatLink } from '@hcengineering/core'
/**
* Configuration options for password-based authentication
* @public
*/
export interface PasswordAuthOptions {
/** User's email address */
email: string
/** User's password */
password: string
/** Workspace name */
workspace: string
}
/**
* Configuration options for token-based authentication
* @public
*/
export interface TokenAuthOptions {
/** Authentication token */
token: string
/** Workspace name */
workspace: string
}
/**
* Union type representing all authentication options
* Can be either password-based or token-based authentication
* @public
*/
export type AuthOptions = PasswordAuthOptions | TokenAuthOptions
/** @public */
export interface LoginInfo {
token: string
endpoint: string
confirmed: boolean
email: string
}
/** @public */
export interface WorkspaceLoginInfo extends LoginInfo {
workspace: string
workspaceId: string
}
export interface ServerConfig {
ACCOUNTS_URL: string
}
async function loadServerConfig (url: string): Promise<ServerConfig> {
const configUrl = concatLink(url, '/config.json')
const res = await fetch(configUrl)
if (res.ok) {
return (await res.json()) as ServerConfig
}
throw new Error('Failed to fetch config')
}
export async function getWorkspaceLogin (
configUrl: string,
options: AuthOptions,
serverConfig?: ServerConfig
): Promise<WorkspaceLoginInfo> {
serverConfig ??= await loadServerConfig(configUrl)
let token: string
if ('token' in options) {
token = options.token
} else {
const { email, password, workspace } = options
token = await login(serverConfig.ACCOUNTS_URL, email, password, workspace)
}
if (token === undefined) {
throw new Error('Login failed')
}
const ws = await selectWorkspace(serverConfig.ACCOUNTS_URL, token, options.workspace)
if (ws === undefined) {
throw new Error('Workspace not found')
}
return ws
}
async function login (accountsUrl: string, user: string, password: string, workspace: string): Promise<string> {
const response = await fetch(accountsUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
method: 'login',
params: [user, password, workspace]
})
})
const result = await response.json()
return result.result?.token
}
async function selectWorkspace (accountsUrl: string, token: string, workspace: string): Promise<WorkspaceLoginInfo> {
const response = await fetch(accountsUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + token
},
body: JSON.stringify({
method: 'selectWorkspace',
params: [workspace, 'external']
})
})
const result = await response.json()
return result.result as WorkspaceLoginInfo
}
-164
View File
@@ -1,164 +0,0 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
type Account,
type AccountClient,
type Class,
type Doc,
type DocumentQuery,
type FindOptions,
type FindResult,
type ModelDb,
type Tx,
type TxResult,
type Ref,
type Hierarchy,
type SearchQuery,
type SearchOptions,
type SearchResult,
type WithLookup
} from '@hcengineering/core'
import { type ConnectOptions } from './types'
import { getWorkspaceLogin } from './account'
import { decodeModel } from './utils'
export async function createHttpClient (httpApiWorkerUrl: string, options: ConnectOptions): Promise<AccountClient> {
let token = options.workspaceToken
if (token === undefined) {
if (options.authOptions === undefined) {
throw new Error('Either workspaceToken or authOptions must be provided')
}
if (options.configUrl === '' && options.serverConfig === undefined) {
throw new Error('Either configUrl or serverConfig must be provided')
}
const ws = await getWorkspaceLogin(options.configUrl ?? '', options.authOptions, options.serverConfig)
token = ws.token
}
const client = new TransactorHttpClient(token, options.workspaceId ?? '', httpApiWorkerUrl)
if (options.loadModel ?? false) {
await client.loadModel()
}
return client
}
class TransactorHttpClient implements AccountClient {
private model: ModelDb | undefined
private hierarchy: Hierarchy | undefined
constructor (
private readonly token: string,
private readonly workspaceId: string,
private readonly httpApiWorkerUrl: string
) {}
private url (method: string): string {
return `${this.httpApiWorkerUrl}/${method}/${encodeURIComponent(this.workspaceId)}`
}
async loadModel (): Promise<void> {
const response = await fetch(this.url('model'), {
method: 'GET',
headers: {
Accept: 'application/octet-stream',
Authorization: 'Bearer ' + this.token
}
})
const compressed = await (response as any).bytes()
const { model, hierarchy } = await decodeModel(compressed)
this.model = model
this.hierarchy = hierarchy
}
notify (...tx: Tx[]): void {
// does nothing
}
getHierarchy (): Hierarchy {
if (this.hierarchy === undefined) {
throw new Error('Hierarchy is not loaded, please use loadModel=true when initializing client')
}
return this.hierarchy
}
getModel (): ModelDb {
if (this.model === undefined) {
throw new Error('Model is not loaded, please use loadModel=true when initializing client')
}
return this.model
}
async findOne<T extends Doc>(
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
): Promise<WithLookup<T> | undefined> {
return (await this.findAll(_class, query, options)).shift()
}
async close (): Promise<void> {
// does nothing
}
async findAll<T extends Doc>(
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
): Promise<FindResult<T>> {
const response = await fetch(this.url('find-all'), {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: 'Bearer ' + this.token
},
body: JSON.stringify({ _class, query, options })
})
return await response.json()
}
async tx (tx: Tx): Promise<TxResult> {
const response = await fetch(this.url('tx'), {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: 'Bearer ' + this.token
},
body: JSON.stringify(tx)
})
return await response.json()
}
async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
// TODO
const result: SearchResult = {
docs: [],
total: 0
}
return result
}
async getAccount (): Promise<Account> {
const response = await fetch(this.url('account'), {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer ' + this.token
}
})
return await response.json()
}
}
-20
View File
@@ -1,20 +0,0 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
export * from './http'
export * from './account'
export * from './rpc'
export * from './types'
export * from './utils'
-169
View File
@@ -1,169 +0,0 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
type Account,
type AccountClient,
type Class,
type Doc,
type DocumentQuery,
type FindOptions,
type FindResult,
type Ref,
type SearchOptions,
type SearchQuery,
type SearchResult,
type Storage,
type Tx,
type TxResult,
type WithLookup,
type Hierarchy,
type ModelDb
} from '@hcengineering/core'
import { decodeModel } from './utils'
import { type ConnectOptions } from './types'
import { getWorkspaceLogin } from './account'
export interface TransactorService {
openRpc: (token: string, workspaceId: string) => Promise<TransactorRawApi>
}
export interface TransactorRawApi extends Storage {
getModel: () => Promise<Buffer>
getAccount: () => Promise<Account>
}
export async function createRpcClient (
transactorService: TransactorService,
options: ConnectOptions
): Promise<AccountClient> {
let token = options.workspaceToken
if (token === undefined) {
if (options.authOptions === undefined) {
throw new Error('Either workspaceToken or authOptions must be provided')
}
if (options.configUrl === '' && options.serverConfig === undefined) {
throw new Error('Either configUrl or serverConfig must be provided')
}
const ws = await getWorkspaceLogin(options.configUrl ?? '', options.authOptions, options.serverConfig)
token = ws.token
}
const client = new TransactorRpcClient(token, options.workspaceId ?? '', transactorService)
if (options.loadModel === true) {
await client.loadModel()
}
return client
}
class TransactorRpcClient implements AccountClient {
private disposed = false
private model: ModelDb | undefined
private hierarchy: Hierarchy | undefined
private transactorRpcStub: TransactorRawApi | undefined
private account: Account | undefined
constructor (
private readonly token: string,
private readonly workspaceId: string,
private readonly transactorService: TransactorService
) {}
private async transactorStub (): Promise<TransactorRawApi> {
if (this.transactorRpcStub === undefined) {
this.transactorRpcStub = await this.transactorService.openRpc(this.token, this.workspaceId)
}
return this.transactorRpcStub
}
async loadModel (): Promise<void> {
const stub = await this.transactorStub()
const compressed = await stub.getModel()
const { model, hierarchy } = await decodeModel(compressed)
this.model = model
this.hierarchy = hierarchy
}
notify (...tx: Tx[]): void {
// does nothing
}
getHierarchy (): Hierarchy {
if (this.hierarchy === undefined) {
throw new Error('Hierarchy is not loaded, please use loadModel=true when initializing client')
}
return this.hierarchy
}
getModel (): ModelDb {
if (this.model === undefined) {
throw new Error('Model is not loaded, please use loadModel=true when initializing client')
}
return this.model
}
async findOne<T extends Doc>(
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
): Promise<WithLookup<T> | undefined> {
return (await this.findAll(_class, query, options)).shift()
}
async close (): Promise<void> {
this.dispose()
}
async findAll<T extends Doc>(
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
): Promise<FindResult<T>> {
const stub = await this.transactorStub()
return await stub.findAll(_class, query, options)
}
async tx (tx: Tx): Promise<TxResult> {
const stub = await this.transactorStub()
return await stub.tx(tx)
}
async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
// TODO
const result: SearchResult = {
docs: [],
total: 0
}
return result
}
async getAccount (): Promise<Account> {
if (this.account === undefined) {
const stub = await this.transactorStub()
this.account = await stub.getAccount()
}
return this.account
}
private dispose (): void {
if (!this.disposed && this.transactorRpcStub !== undefined && Symbol.dispose in this.transactorRpcStub) {
this.disposed = true
;(this.transactorRpcStub as any)[Symbol.dispose]()
}
}
[Symbol.dispose] (): void {
this.dispose()
}
}
-30
View File
@@ -1,30 +0,0 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { type AuthOptions, type ServerConfig } from './account'
export interface ConnectOptions {
workspaceToken?: string
workspaceId?: string
loadModel?: boolean
authOptions: AuthOptions
configUrl?: string
serverConfig?: ServerConfig
}
-96
View File
@@ -1,96 +0,0 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { gunzip } from 'zlib'
import { promisify } from 'util'
import {
Hierarchy,
ModelDb,
type MeasureContext,
type MeasureLogger,
type ParamsType,
type Tx
} from '@hcengineering/core'
export async function unpackModel (compressed: Buffer | Uint8Array): Promise<Tx[]> {
const ungzipAsync = promisify(gunzip)
const buffer = await ungzipAsync(new Uint8Array(compressed))
const decoder = new TextDecoder()
const jsonString = decoder.decode(buffer)
const model = JSON.parse(jsonString) as Tx[]
return model
}
export async function decodeModel (compressed: Buffer | Uint8Array): Promise<{ model: ModelDb, hierarchy: Hierarchy }> {
const txes = await unpackModel(compressed)
const hierarchy = new Hierarchy()
for (const tx of txes) {
hierarchy.tx(tx)
}
const model = new ModelDb(hierarchy)
const ctx = createDummyMeasureContext()
model.addTxes(ctx, txes, false)
return { model, hierarchy }
}
function createConsoleLogger (): MeasureLogger {
return {
info: (message: string, obj?: Record<string, any>) => {
console.info(message, obj)
},
error: (message: string, obj?: Record<string, any>) => {
console.error(message, obj)
},
warn: (message: string, obj?: Record<string, any>) => {
console.warn(message, obj)
},
logOperation: (operation: string, time: number, params: ParamsType) => {
console.info(operation, { time, ...params })
},
close: async () => {}
}
}
function createDummyMeasureContext (): MeasureContext {
const ctx: MeasureContext = {
id: '',
contextData: {},
newChild: (name, params, fullParams, logger) => {
return ctx
},
with: <T>(name: any, params: any, op: any, fullParams: any) => {
return Promise.resolve() as Promise<T>
},
withSync: (name, params, op, fullParams) => {
return op(ctx)
},
withLog: <T>(name: any, params: any, op: any, fullParams: any) => {
return Promise.resolve() as Promise<T>
},
logger: createConsoleLogger(),
measure: (name, value, override) => {},
error: (message, args) => {
console.error(message, args)
},
info: (message, args) => {
console.info(message, args)
},
warn: (message, args) => {
console.warn(message, args)
},
end: () => {}
}
return ctx
}
-10
View File
@@ -1,10 +0,0 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
}
}
-7
View File
@@ -1,7 +0,0 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
@@ -1,4 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig"
}
@@ -1,7 +0,0 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
-45
View File
@@ -1,45 +0,0 @@
{
"name": "@hcengineering/cloud-transactor-http-api",
"version": "0.6.0",
"main": "lib/index.js",
"types": "types/index.d.ts",
"template": "cloud",
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev --port 4040",
"dev-local": "wrangler dev --port 4040 --local --upstream-protocol=http",
"start": "wrangler dev --port 4040",
"cf-typegen": "wrangler types",
"build": "compile",
"build:watch": "compile",
"test": "jest --passWithNoTests --silent --forceExit",
"format": "format src",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent --forceExit",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20241022.0",
"@hcengineering/platform-rig": "^0.6.0",
"@types/jest": "^29.5.5",
"@types/node": "~20.11.16",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"jest": "^29.7.0",
"prettier": "^3.1.0",
"ts-jest": "^29.1.1",
"typescript": "^5.3.3",
"wrangler": "^3.97.0"
},
"dependencies": {
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/core": "^0.6.32",
"itty-router": "^5.0.18"
}
}
-125
View File
@@ -1,125 +0,0 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { type Account, type Storage } from '@hcengineering/core'
import { WorkerEntrypoint } from 'cloudflare:workers'
import { type IRequest, type IRequestStrict, type RequestHandler, Router, cors, error, html, json } from 'itty-router'
const { preflight, corsify } = cors({
maxAge: 86400
})
const router = Router<IRequestStrict, [Env, ExecutionContext], Response>({
before: [preflight],
finally: [corsify]
})
type WorkspaceRequest = {
workspaceId: string
token: string
} & IRequestStrict
interface TransactorService {
openRpc: (token: string, workspaceId: string) => Promise<TransactorRawApi>
}
interface TransactorRawApi extends Storage {
getModel: () => Promise<Buffer>
getAccount: () => Promise<Account>
}
const withWorkspace: RequestHandler<WorkspaceRequest> = (request: WorkspaceRequest) => {
if (request.params.workspaceId === undefined || request.params.workspaceId === '') {
return error(400, 'Missing workspace')
}
const token = request.headers.get('Authorization')
if (token === null) {
return error(401, 'Missing Authorization header')
}
request.workspaceId = decodeURIComponent(request.params.workspaceId)
request.token = token.split(' ')[1]
}
async function callTransactor (
request: WorkspaceRequest,
env: Env,
method: (transactorRpc: TransactorRawApi) => Promise<Response>
): Promise<Response> {
const transactorService = env.TRANSACTOR_SERVICE as any as TransactorService
const transactorRpc = await transactorService.openRpc(request.token, request.workspaceId)
try {
return await method(transactorRpc)
} finally {
if (Symbol.dispose in transactorRpc) {
;(transactorRpc as any)[Symbol.dispose]()
}
}
}
router.post('/find-all/:workspaceId', withWorkspace, async (request: WorkspaceRequest, env: Env) => {
return await callTransactor(request, env, async (transactorRpc) => {
if (request.body === null) {
return error(400, 'Missing body')
}
const { _class, query, options }: any = await request.json()
const result = await transactorRpc.findAll(_class, query, options)
return json(result)
})
})
router.post('/tx/:workspaceId', withWorkspace, async (request: WorkspaceRequest, env: Env) => {
return await callTransactor(request, env, async (transactorRpc) => {
if (request.body === null) {
return error(400, 'Missing body')
}
const tx: any = await request.json()
const result = await transactorRpc.tx(tx)
return json(result)
})
})
router.get('/model/:workspaceId', withWorkspace, async (request: WorkspaceRequest, env: Env) => {
return await callTransactor(request, env, async (transactorRpc) => {
const model = await transactorRpc.getModel()
return new Response(model, {
headers: {
'Content-Type': 'application/octet-stream',
'Content-Length': model.length.toString()
}
})
})
})
router.get('/account/:workspaceId', withWorkspace, async (request: WorkspaceRequest, env: Env) => {
return await callTransactor(request, env, async (transactorRpc) => {
const account = await transactorRpc.getAccount()
return json(account)
})
})
router.all('/', () =>
html(
`Huly&reg; Transactor API&trade; <a href="https://huly.io">https://huly.io</a>
&copy; 2024 <a href="https://hulylabs.com">Huly Labs</a>`
)
)
router.all('*', () => error(404))
export default class TransactorHttpApiWorker extends WorkerEntrypoint {
async fetch (request: IRequest): Promise<Response> {
return await router.fetch(request, this.env, this.ctx).catch(error)
}
}
-11
View File
@@ -1,11 +0,0 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo",
"types": ["@cloudflare/workers-types", "node", "jest"]
}
}
-5
View File
@@ -1,5 +0,0 @@
// Generated by Wrangler by running `wrangler types`
interface Env {
TRANSACTOR_SERVICE: Fetcher;
}
-11
View File
@@ -1,11 +0,0 @@
#:schema node_modules/wrangler/config-schema.json
name = "cloud-transactor-http-api"
main = "src/index.ts"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
keep_vars = true
[[services]]
binding = "TRANSACTOR_SERVICE"
service = "cloud-transactor"
entrypoint = "TransactorRpc"