diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 00b1ff93cb..c0a3e82da1 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -2,6 +2,7 @@ lockfileVersion: 5.3 specifiers: '@microsoft/api-extractor': ^7.18.4 + '@rush-temp/client': file:./projects/client.tgz '@rush-temp/core': file:./projects/core.tgz '@rush-temp/dev-server': file:./projects/dev-server.tgz '@rush-temp/dev-storage': file:./projects/dev-storage.tgz @@ -12,11 +13,13 @@ specifiers: '@rush-temp/platform-rig': file:./projects/platform-rig.tgz '@rush-temp/presentation': file:./projects/presentation.tgz '@rush-temp/prod': file:./projects/prod.tgz + '@rush-temp/query': file:./projects/query.tgz '@rush-temp/server': file:./projects/server.tgz '@rush-temp/server-core': file:./projects/server-core.tgz '@rush-temp/server-ws': file:./projects/server-ws.tgz '@rush-temp/theme': file:./projects/theme.tgz '@rush-temp/ui': file:./projects/ui.tgz + '@rush-temp/view': file:./projects/view.tgz '@rush-temp/workbench': file:./projects/workbench.tgz '@rush-temp/workbench-resources': file:./projects/workbench-resources.tgz '@rushstack/heft': ^0.35.0 @@ -57,6 +60,7 @@ specifiers: dependencies: '@microsoft/api-extractor': 7.18.4 + '@rush-temp/client': file:projects/client.tgz_6c259fadfeb3a4b20890aefe87070b8b '@rush-temp/core': file:projects/core.tgz_6c259fadfeb3a4b20890aefe87070b8b '@rush-temp/dev-server': file:projects/dev-server.tgz_6c259fadfeb3a4b20890aefe87070b8b '@rush-temp/dev-storage': file:projects/dev-storage.tgz_6c259fadfeb3a4b20890aefe87070b8b @@ -67,11 +71,13 @@ dependencies: '@rush-temp/platform-rig': file:projects/platform-rig.tgz_6ab28797e7a22071465f7d680ae81ae5 '@rush-temp/presentation': file:projects/presentation.tgz_c38cf1a7a413db8918b0b4754c21e4c5 '@rush-temp/prod': file:projects/prod.tgz_sass@1.37.5+typescript@4.3.5 + '@rush-temp/query': file:projects/query.tgz_6c259fadfeb3a4b20890aefe87070b8b '@rush-temp/server': file:projects/server.tgz_6c259fadfeb3a4b20890aefe87070b8b '@rush-temp/server-core': file:projects/server-core.tgz_6c259fadfeb3a4b20890aefe87070b8b '@rush-temp/server-ws': file:projects/server-ws.tgz_6c259fadfeb3a4b20890aefe87070b8b '@rush-temp/theme': file:projects/theme.tgz_c38cf1a7a413db8918b0b4754c21e4c5 '@rush-temp/ui': file:projects/ui.tgz_c38cf1a7a413db8918b0b4754c21e4c5 + '@rush-temp/view': file:projects/view.tgz_6c259fadfeb3a4b20890aefe87070b8b '@rush-temp/workbench': file:projects/workbench.tgz_6c259fadfeb3a4b20890aefe87070b8b '@rush-temp/workbench-resources': file:projects/workbench-resources.tgz_c38cf1a7a413db8918b0b4754c21e4c5 '@rushstack/heft': 0.35.0 @@ -7680,6 +7686,24 @@ packages: commander: 2.20.3 dev: false + file:projects/client.tgz_6c259fadfeb3a4b20890aefe87070b8b: + resolution: {integrity: sha512-8YJou7AitxzzH2roWCno/zgd27SiQXlqrdDVyhAAvZrBZY7KGbxw4OxGkI+6eVK+Lk/zajS2FkGZ+oyZ+Bxe6g==, tarball: file:projects/client.tgz} + id: file:projects/client.tgz + name: '@rush-temp/client' + version: 0.0.0 + dependencies: + '@types/heft-jest': 1.0.2 + '@typescript-eslint/eslint-plugin': 4.28.5_a8e83fcad666e1ba86be4b2e27a20aea + eslint: 7.32.0 + eslint-plugin-import: 2.23.4_eslint@7.32.0 + eslint-plugin-node: 11.1.0_eslint@7.32.0 + eslint-plugin-promise: 4.3.1 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - supports-color + - typescript + dev: false + file:projects/core.tgz_6c259fadfeb3a4b20890aefe87070b8b: resolution: {integrity: sha512-H49oedby3HV+eR2uQZAI0kIJ8Yxh8uIR+sLg4Ox0JAy4UJtfP5AmyWltddqaUdQIcfYrb2BrhDSDSxddlDWhpg==, tarball: file:projects/core.tgz} id: file:projects/core.tgz @@ -7827,7 +7851,7 @@ packages: dev: false file:projects/presentation.tgz_c38cf1a7a413db8918b0b4754c21e4c5: - resolution: {integrity: sha512-y4BqVd0TM1NHy74NaREi5s26qWOro+kn+dVogNYFm3mkuVEqHwv/ZzivbLI9X7Ti7IbuG8FeCy2N1r9Cs6XUJg==, tarball: file:projects/presentation.tgz} + resolution: {integrity: sha512-v1qRQGQrWDFfME4IEpK6bM0E3IQljeG2+Ft/YVPM/Fbf/y3e+28AvI3nyvLn+1N6gY+5faEAHLE4M9vBbR+A1A==, tarball: file:projects/presentation.tgz} id: file:projects/presentation.tgz name: '@rush-temp/presentation' version: 0.0.0 @@ -7887,6 +7911,25 @@ packages: - utf-8-validate dev: false + file:projects/query.tgz_6c259fadfeb3a4b20890aefe87070b8b: + resolution: {integrity: sha512-9CUYPnThJmot4jmOsi6R7ugG/yTz+6emWtPfhSTK+1hqr3hXaYSXIXuYirvqpa6a434uRTUAe362U5VuNi4xdA==, tarball: file:projects/query.tgz} + id: file:projects/query.tgz + name: '@rush-temp/query' + version: 0.0.0 + dependencies: + '@types/heft-jest': 1.0.2 + '@typescript-eslint/eslint-plugin': 4.28.5_a8e83fcad666e1ba86be4b2e27a20aea + eslint: 7.32.0 + eslint-plugin-import: 2.23.4_eslint@7.32.0 + eslint-plugin-node: 11.1.0_eslint@7.32.0 + eslint-plugin-promise: 4.3.1 + simplytyped: 3.3.0_typescript@4.3.5 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - supports-color + - typescript + dev: false + file:projects/server-core.tgz_6c259fadfeb3a4b20890aefe87070b8b: resolution: {integrity: sha512-lHE4PAWrX+WKdM9/Yy1rYZW6rCacwLMkSUIJakrXZeyGlNytXXSvH20bgfocVn73grez3Zjy6qlNm7H7EClgDQ==, tarball: file:projects/server-core.tgz} id: file:projects/server-core.tgz @@ -7997,8 +8040,26 @@ packages: - typescript dev: false + file:projects/view.tgz_6c259fadfeb3a4b20890aefe87070b8b: + resolution: {integrity: sha512-BQgcAfVRNy1QO/Z8ATfQZbm090HU2b7jl5r38stX7rjzF0udaYKci9yDb9xECP1oYCEPWXbayUPlF/AwRqxZ4g==, tarball: file:projects/view.tgz} + id: file:projects/view.tgz + name: '@rush-temp/view' + version: 0.0.0 + dependencies: + '@types/heft-jest': 1.0.2 + '@typescript-eslint/eslint-plugin': 4.28.5_a8e83fcad666e1ba86be4b2e27a20aea + eslint: 7.32.0 + eslint-plugin-import: 2.23.4_eslint@7.32.0 + eslint-plugin-node: 11.1.0_eslint@7.32.0 + eslint-plugin-promise: 4.3.1 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - supports-color + - typescript + dev: false + file:projects/workbench-resources.tgz_c38cf1a7a413db8918b0b4754c21e4c5: - resolution: {integrity: sha512-LMzBHdlTssEH28+ttT/0/eeghU3OWwwJ7AWksnnJsabUEgfjaYNZqTfsLiD6CCjMJXMVU/kUvUjBmlpB8HWUpg==, tarball: file:projects/workbench-resources.tgz} + resolution: {integrity: sha512-UZMkx/Mq9SBiSR6JFaCmJ548KfR1cSser177G9y196X9ot7EYLYHSZ0Teo9hZTwUgDU1TDkk2KW2sxuFN71naA==, tarball: file:projects/workbench-resources.tgz} id: file:projects/workbench-resources.tgz name: '@rush-temp/workbench-resources' version: 0.0.0 diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 9345ceae1b..7b82e6d306 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -20,7 +20,6 @@ import type { Storage, DocumentQuery, FindOptions, FindResult } from './storage' import { Hierarchy } from './hierarchy' import { ModelDb } from './memdb' import { DOMAIN_MODEL } from './classes' -import { TxProcessor } from './tx' import core from './component' @@ -36,9 +35,8 @@ export interface Client extends Storage { getHierarchy: () => Hierarchy } -class ClientImpl extends TxProcessor implements Storage, Client { +class ClientImpl implements Storage, Client { constructor (private readonly hierarchy: Hierarchy, private readonly model: ModelDb, private readonly conn: Storage) { - super() } getHierarchy (): Hierarchy { return this.hierarchy } diff --git a/packages/presentation/img/chen.png b/packages/presentation/img/chen.png new file mode 100755 index 0000000000..9f9d110791 Binary files /dev/null and b/packages/presentation/img/chen.png differ diff --git a/packages/presentation/package.json b/packages/presentation/package.json index 137b19cc05..88cf07199f 100644 --- a/packages/presentation/package.json +++ b/packages/presentation/package.json @@ -19,7 +19,11 @@ "svelte-preprocess":"^4.7.4" }, "dependencies": { + "@anticrm/platform": "~0.6.0", + "@anticrm/core": "~0.6.0", + "@anticrm/query": "~0.6.0", "@anticrm/ui": "~0.6.0", + "@anticrm/view": "~0.6.0", "svelte": "^3.37.0" } } diff --git a/packages/presentation/src/utils.ts b/packages/presentation/src/utils.ts index 3571530b1e..0bc324564e 100644 --- a/packages/presentation/src/utils.ts +++ b/packages/presentation/src/utils.ts @@ -16,17 +16,20 @@ import { getContext, setContext, onDestroy } from 'svelte' -import type { Doc, Ref, Class, DocumentQuery, FindOptions } from '@anticrm/core' -import type { Connection } from '@anticrm/client' +import type { Doc, Ref, Class, DocumentQuery, FindOptions, Client } from '@anticrm/core' +import { LiveQuery as LQ } from '@anticrm/query' const CLIENT_CONEXT = 'workbench.context.Client' -export function getClient(): Connection { - return getContext(CLIENT_CONEXT) +let liveQuery: LQ + +export function getClient(): Client { + return getContext(CLIENT_CONEXT) } -export function setClient(client: Connection) { +export function setClient(client: Client) { setContext(CLIENT_CONEXT, client) + liveQuery = new LQ(client) } class LiveQuery { @@ -39,7 +42,7 @@ class LiveQuery { query(_class: Ref>, query: DocumentQuery, callback: (result: T[]) => void, options?: FindOptions) { this.unsubscribe() - this.unsubscribe = this.client.query(_class, query, callback, options) + this.unsubscribe = liveQuery.query(_class, query, callback, options) } } diff --git a/packages/query/.eslintrc.js b/packages/query/.eslintrc.js new file mode 100644 index 0000000000..89f8151bd4 --- /dev/null +++ b/packages/query/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + extends: ['./node_modules/@anticrm/platform-rig/profiles/default/config/eslint.config.json'], + parserOptions: { + project: './tsconfig.json' + } +} \ No newline at end of file diff --git a/packages/query/.npmignore b/packages/query/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/packages/query/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/packages/query/config/rig.json b/packages/query/config/rig.json new file mode 100644 index 0000000000..af1257a896 --- /dev/null +++ b/packages/query/config/rig.json @@ -0,0 +1,18 @@ +// The "rig.json" file directs tools to look for their config files in an external package. +// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + + /** + * (Required) The name of the rig package to inherit from. + * It should be an NPM package name with the "-rig" suffix. + */ + "rigPackageName": "@anticrm/platform-rig" + + /** + * (Optional) Selects a config profile from the rig package. The name must consist of + * lowercase alphanumeric words separated by hyphens, for example "sample-profile". + * If omitted, then the "default" profile will be used." + */ + // "rigProfile": "your-profile-name" +} diff --git a/packages/query/package.json b/packages/query/package.json new file mode 100644 index 0000000000..503b02433e --- /dev/null +++ b/packages/query/package.json @@ -0,0 +1,25 @@ +{ + "name": "@anticrm/query", + "version": "0.6.0", + "main": "lib/index.js", + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "heft build", + "lint:fix": "eslint --fix src" + }, + "devDependencies": { + "@anticrm/platform-rig":"~0.6.0", + "@types/heft-jest":"^1.0.2", + "@typescript-eslint/eslint-plugin":"4", + "eslint-plugin-import":"2", + "eslint-plugin-promise":"4", + "eslint-plugin-node":"11", + "eslint":"^7.32.0", + "simplytyped": "^3.3.0" + }, + "dependencies": { + "@anticrm/platform": "~0.6.3", + "@anticrm/core": "~0.6.3" + } +} diff --git a/packages/query/src/__tests__/connection.txt b/packages/query/src/__tests__/connection.txt new file mode 100644 index 0000000000..279d621538 --- /dev/null +++ b/packages/query/src/__tests__/connection.txt @@ -0,0 +1,50 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// 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 { Tx, Storage, Ref, Doc, Class, DocumentQuery, FindResult } from '@anticrm/core' +import core, { ModelDb, TxDb, Hierarchy, DOMAIN_TX } from '@anticrm/core' + +import { genMinModel } from '@anticrm/core/src/__tests__/minmodel' + +export async function connect (handler: (tx: Tx) => void): Promise { + const txes = genMinModel() + + const hierarchy = new Hierarchy() + for (const tx of txes) hierarchy.tx(tx) + + const transactions = new TxDb(hierarchy) + const model = new ModelDb(hierarchy) + for (const tx of txes) { + await transactions.tx(tx) + await model.tx(tx) + } + + async function findAll (_class: Ref>, query: DocumentQuery): Promise> { + const domain = hierarchy.getClass(_class).domain + if (domain === DOMAIN_TX) return await transactions.findAll(_class, query) + return await model.findAll(_class, query) + } + + return { + findAll, + tx: async (tx: Tx): Promise => { + if (tx.objectSpace === core.space.Model) { + hierarchy.tx(tx) + } + await Promise.all([model.tx(tx), transactions.tx(tx)]) + handler(tx) + } + } +} diff --git a/packages/query/src/__tests__/query.test.txt b/packages/query/src/__tests__/query.test.txt new file mode 100644 index 0000000000..c5fe654020 --- /dev/null +++ b/packages/query/src/__tests__/query.test.txt @@ -0,0 +1,263 @@ +// +// Copyright © 2021 Anticrm Platform Contributors. +// +// 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 { Class, Client, Doc, DocumentQuery, FindOptions, FindResult, Obj, Ref, Space, Tx, TxCreateDoc } from '@anticrm/core' +import core, { createClient, DOMAIN_TX, Hierarchy, ModelDb, TxDb, withOperations, SortingOrder } from '@anticrm/core' +import { genMinModel as getModel } from '@anticrm/core/src/__tests__/minmodel' +import { LiveQuery } from '..' +import { connect } from './connection' + +interface Channel extends Space { + x: number +} +describe('query', () => { + it('findAll', async () => { + const client = await getClient() + const query = withOperations(core.account.System, new LiveQuery(client)) + const result = await query.findAll(core.class.Space, {}) + expect(result).toHaveLength(2) + }) + + it('query with param', async (done) => { + const storage = await getClient() + + let expectedLength = 0 + const txes = await getModel() + for (let i = 0; i < txes.length; i++) { + if (storage.getHierarchy().isDerived((txes[i] as TxCreateDoc).objectClass, core.class.Space)) { + expectedLength++ + } + } + + const query = new LiveQuery(storage) + query.query(core.class.Space, { private: false }, (result) => { + expect(result).toHaveLength(expectedLength) + done() + }) + }) + + it('query should be live', async (done) => { + const storage = await getClient() + + let expectedLength = 0 + const txes = await getModel() + for (let i = 0; i < txes.length; i++) { + if (storage.getHierarchy().isDerived((txes[i] as TxCreateDoc).objectClass, core.class.Space)) { + expectedLength++ + } + } + + let attempt = 0 + const query = withOperations(core.account.System, new LiveQuery(storage)) + query.query(core.class.Space, { private: false }, (result) => { + expect(result).toHaveLength(expectedLength + attempt) + if (attempt > 0) { + expect((result[expectedLength + attempt - 1] as any).x).toBe(attempt) + } + if (attempt++ === 3) { + // check underlying storage received all data. + storage + .findAll(core.class.Space, { private: false }) + .then((result) => { + expect(result).toHaveLength(expectedLength + attempt - 1) + done() + }) + .catch((err) => expect(err).toBeUndefined()) + } + }) + + await query.createDoc(core.class.Space, core.space.Model, { + private: false, + name: '#1', + description: '', + members: [], + x: 1 + }) + await query.createDoc(core.class.Space, core.space.Model, { + private: false, + name: '#2', + description: '', + members: [], + x: 2 + }) + await query.createDoc(core.class.Space, core.space.Model, { + private: false, + name: '#3', + description: '', + members: [], + x: 3 + }) + }) + + it('unsubscribe query', async () => { + const storage = await getClient() + + let expectedLength = 0 + const txes = await getModel() + for (let i = 0; i < txes.length; i++) { + if (storage.getHierarchy().isDerived((txes[i] as TxCreateDoc).objectClass, core.class.Space)) { + expectedLength++ + } + } + + const query = withOperations(core.account.System, new LiveQuery(storage)) + const unsubscribe = query.query(core.class.Space, { private: false }, (result) => { + expect(result).toHaveLength(expectedLength) + }) + + unsubscribe() + + await query.createDoc(core.class.Space, core.space.Model, { + private: false, + name: '#1', + description: '', + members: [] + }) + await query.createDoc(core.class.Space, core.space.Model, { + private: false, + name: '#2', + description: '', + members: [] + }) + await query.createDoc(core.class.Space, core.space.Model, { + private: false, + name: '#3', + description: '', + members: [] + }) + }) + + it('query against core client', async (done) => { + const client = await createClient(connect) + + const expectedLength = 2 + let attempt = 0 + const query = withOperations(core.account.System, new LiveQuery(client)) + query.query(core.class.Space, { private: false }, (result) => { + expect(result).toHaveLength(expectedLength + attempt) + if (attempt > 0) { + expect((result[expectedLength + attempt - 1] as any).x).toBe(attempt) + } + if (attempt++ === 1) done() + }) + + await query.createDoc(core.class.Space, core.space.Model, { + x: 1, + private: false, + name: '#1', + description: '', + members: [] + }) + await query.createDoc(core.class.Space, core.space.Model, { + x: 2, + private: false, + name: '#2', + description: '', + members: [] + }) + await query.createDoc(core.class.Space, core.space.Model, { + x: 3, + private: false, + name: '#3', + description: '', + members: [] + }) + }) + + it('limit and sorting', async (done) => { + const storage = await getClient() + + const limit = 1 + let attempt = 0 + let doneCount = 0 + + const query = withOperations(core.account.System, new LiveQuery(storage)) + query.query(core.class.Space, { private: true }, (result) => { + if (attempt > 0 && result.length > 0) { + expect(result.length).toEqual(limit) + expect(result[0].name).toMatch('0') + } + if (attempt === 1) doneCount++ + if (doneCount === 2) done() + }, { limit: limit, sort: { name: SortingOrder.Ascending } }) + + query.query(core.class.Space, { private: true }, (result) => { + if (attempt > 0 && result.length > 0) { + expect(result.length).toEqual(limit) + expect(result[0].name).toMatch(attempt.toString()) + } + if (attempt === 10) doneCount++ + if (doneCount === 2) done() + }, { limit: limit, sort: { name: SortingOrder.Descending } }) + + for (let i = 0; i < 10; i++) { + attempt++ + await query.createDoc(core.class.Space, core.space.Model, { + private: true, + name: i.toString(), + description: '', + members: [] + }) + } + }) + it('remove', async (done) => { + const client = await createClient(connect) + + const expectedLength = 2 + let attempt = 0 + const query = withOperations(core.account.System, new LiveQuery(client)) + query.query(core.class.Space, { private: false }, (result) => { + expect(result).toHaveLength(expectedLength - attempt) + if (attempt++ === expectedLength) done() + }) + + const spaces = await query.findAll(core.class.Space, {}) + for (const space of spaces) { + await query.removeDoc(space._class, space.space, space._id) + } + }) +}) + +class ClientImpl implements Client { + constructor ( + private readonly hierarchy: Hierarchy, + private readonly model: ModelDb, + private readonly transactions: TxDb + ) {} + + async tx (tx: Tx): Promise { + await Promise.all([this.model.tx(tx), this.transactions.tx(tx)]) + } + + getHierarchy(): Hierarchy { + return this.hierarchy + } + + async findAll(_class: Ref>, query: DocumentQuery, options?: FindOptions): Promise> { + const domain = this.hierarchy.getClass(_class).domain + if (domain === DOMAIN_TX) return await this.transactions.findAll(_class, query, options) + return await this.model.findAll(_class, query, options) + } +} + +async function getClient (): Promise { + const hierarchy = new Hierarchy() + const transactions = new TxDb(hierarchy) + const model = new ModelDb(hierarchy) + const txes = await getModel() + for (const tx of txes) hierarchy.tx(tx) + for (const tx of txes) await model.tx(tx) + return new ClientImpl(hierarchy, model, transactions) +} diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts new file mode 100644 index 0000000000..421e526210 --- /dev/null +++ b/packages/query/src/index.ts @@ -0,0 +1,202 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// +// 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 { + Ref, Class, Doc, Tx, DocumentQuery, TxCreateDoc, TxRemoveDoc, Client, + FindOptions, TxUpdateDoc, _getOperator, TxProcessor, resultSort, SortingQuery, + FindResult, Hierarchy, Refs, WithLookup, LookupData +} from '@anticrm/core' + +interface Query { + _class: Ref> + query: DocumentQuery + result: Doc[] | Promise + options?: FindOptions + callback: (result: FindResult) => void +} + +export class LiveQuery extends TxProcessor implements Client { + private readonly client: Client + private readonly queries: Query[] = [] + + constructor (client: Client) { + super() + this.client = client + } + + getHierarchy (): Hierarchy { + return this.client.getHierarchy() + } + + private match (q: Query, doc: Doc): boolean { + if (!this.getHierarchy().isDerived(doc._class, q._class)) { + return false + } + for (const key in q.query) { + const value = (q.query as any)[key] + if ((doc as any)[key] !== value) { + return false + } + } + return true + } + + async findAll(_class: Ref>, query: DocumentQuery, options?: FindOptions): Promise> { + return await this.client.findAll(_class, query, options) + } + + query(_class: Ref>, query: DocumentQuery, callback: (result: T[]) => void, options?: FindOptions): () => void { + const result = this.client.findAll(_class, query, options) + const q: Query = { + _class, + query, + result, + options, + callback: callback as (result: Doc[]) => void + } + this.queries.push(q) + result + .then((result) => { + q.callback(result) + }) + .catch((err) => { + console.log('failed to update Live Query: ', err) + }) + + return () => { + this.queries.splice(this.queries.indexOf(q), 1) + } + } + + async txUpdateDoc (tx: TxUpdateDoc): Promise { + for (const q of this.queries) { + if (q.result instanceof Promise) { + q.result = await q.result + } + const updatedDoc = q.result.find(p => p._id === tx.objectId) + if (updatedDoc !== undefined) { + await this.__updateDoc(updatedDoc, tx) + this.sort(q, tx) + await this.callback(updatedDoc, q) + } + } + } + + private async lookup (doc: Doc, lookup: Refs): Promise { + const result: LookupData = {} + for (const key in lookup) { + const _class = (lookup as any)[key] as Ref> + const _id = (doc as any)[key] as Ref + (result as any)[key] = (await this.client.findAll(_class, { _id }))[0] + } + (doc as WithLookup).$lookup = result + } + + async txCreateDoc (tx: TxCreateDoc): Promise { + for (const q of this.queries) { + const doc = TxProcessor.createDoc2Doc(tx) + if (this.match(q, doc)) { + if (q.result instanceof Promise) { + q.result = await q.result + } + + if (q.options?.lookup !== undefined) await this.lookup(doc, q.options.lookup) + + q.result.push(doc) + + if (q.options?.sort !== undefined) resultSort(q.result, q.options?.sort) + + if (q.options?.limit !== undefined && q.result.length > q.options.limit) { + if (q.result.pop()?._id !== doc._id) { + q.callback(q.result) + } + } else { + q.callback(q.result) + } + } + } + } + + async txRemoveDoc (tx: TxRemoveDoc): Promise { + for (const q of this.queries) { + if (q.result instanceof Promise) { + q.result = await q.result + } + const index = q.result.findIndex(p => p._id === tx.objectId) + if (index > -1) { + q.result.splice(index, 1) + q.callback(q.result) + } + } + } + + async tx (tx: Tx): Promise { + await this.client.tx(tx) + await super.tx(tx) + } + + async __updateDoc (updatedDoc: Doc, tx: TxUpdateDoc): Promise { + const ops = tx.operations as any + for (const key in ops) { + if (key.startsWith('$')) { + const operator = _getOperator(key) + operator(updatedDoc, ops[key]) + } else { + (updatedDoc as any)[key] = ops[key] + } + } + updatedDoc.modifiedBy = tx.modifiedBy + updatedDoc.modifiedOn = tx.modifiedOn + } + + private sort (q: Query, tx: TxUpdateDoc): void { + const sort = q.options?.sort + if (sort === undefined) return + let needSort = sort.modifiedBy !== undefined || sort.modifiedOn !== undefined + if (!needSort) needSort = this.checkNeedSort(sort, tx) + + if (needSort) resultSort(q.result as Doc[], sort) + } + + private checkNeedSort (sort: SortingQuery, tx: TxUpdateDoc): boolean { + const ops = tx.operations as any + for (const key in ops) { + if (key.startsWith('$')) { + for (const opKey in ops[key]) { + if (opKey in sort) return true + } + } else { + if (key in sort) return true + } + } + return false + } + + async callback (updatedDoc: Doc, q: Query): Promise { + q.result = q.result as Doc[] + + if (q.options?.limit !== undefined && q.result.length > q.options.limit) { + if (q.result[q.options?.limit]._id === updatedDoc._id) { + const res = await this.findAll(q._class, q.query, q.options) + q.result = res + q.callback(res) + return + } + if (q.result.pop()?._id !== updatedDoc._id) q.callback(q.result) + } else { + q.callback(q.result) + } + } +} diff --git a/packages/query/tsconfig.json b/packages/query/tsconfig.json new file mode 100644 index 0000000000..aeb0517b13 --- /dev/null +++ b/packages/query/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./node_modules/@anticrm/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib" + } +} \ No newline at end of file diff --git a/plugins/client/.eslintrc.js b/plugins/client/.eslintrc.js new file mode 100644 index 0000000000..89f8151bd4 --- /dev/null +++ b/plugins/client/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + extends: ['./node_modules/@anticrm/platform-rig/profiles/default/config/eslint.config.json'], + parserOptions: { + project: './tsconfig.json' + } +} \ No newline at end of file diff --git a/plugins/client/.npmignore b/plugins/client/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/plugins/client/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/plugins/client/config/rig.json b/plugins/client/config/rig.json new file mode 100644 index 0000000000..af1257a896 --- /dev/null +++ b/plugins/client/config/rig.json @@ -0,0 +1,18 @@ +// The "rig.json" file directs tools to look for their config files in an external package. +// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + + /** + * (Required) The name of the rig package to inherit from. + * It should be an NPM package name with the "-rig" suffix. + */ + "rigPackageName": "@anticrm/platform-rig" + + /** + * (Optional) Selects a config profile from the rig package. The name must consist of + * lowercase alphanumeric words separated by hyphens, for example "sample-profile". + * If omitted, then the "default" profile will be used." + */ + // "rigProfile": "your-profile-name" +} diff --git a/plugins/client/package.json b/plugins/client/package.json new file mode 100644 index 0000000000..e42a3a5704 --- /dev/null +++ b/plugins/client/package.json @@ -0,0 +1,24 @@ +{ + "name": "@anticrm/client", + "version": "0.6.0", + "main": "lib/index.js", + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "heft build", + "lint:fix": "eslint --fix src" + }, + "devDependencies": { + "@anticrm/platform-rig":"~0.6.0", + "@types/heft-jest":"^1.0.2", + "@typescript-eslint/eslint-plugin":"4", + "eslint-plugin-import":"2", + "eslint-plugin-promise":"4", + "eslint-plugin-node":"11", + "eslint":"^7.32.0" + }, + "dependencies": { + "@anticrm/platform":"~0.6.3", + "@anticrm/core":"~0.6.0" + } +} diff --git a/plugins/client/src/index.ts b/plugins/client/src/index.ts new file mode 100644 index 0000000000..1f77472ca1 --- /dev/null +++ b/plugins/client/src/index.ts @@ -0,0 +1,37 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// 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 { plugin } from '@anticrm/platform' +import type { Metadata, Plugin, Resource } from '@anticrm/platform' +import type { Client } from '@anticrm/core' +// import type { LiveQuery } from '@anticrm/query' + +// export type Connection = Client & LiveQuery & TxOperations + +/** + * @public + */ +export const clientId = 'client' as Plugin + +export default plugin(clientId, + { + function: { + GetClient: '' as Resource<() => Promise> + }, + metadata: { + ClientUrl: '' as Metadata + } + } +) diff --git a/plugins/client/tsconfig.json b/plugins/client/tsconfig.json new file mode 100644 index 0000000000..32045300ce --- /dev/null +++ b/plugins/client/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./node_modules/@anticrm/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "lib": ["esnext", "dom"] + } +} \ No newline at end of file diff --git a/plugins/view/.eslintrc.js b/plugins/view/.eslintrc.js new file mode 100644 index 0000000000..89f8151bd4 --- /dev/null +++ b/plugins/view/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + extends: ['./node_modules/@anticrm/platform-rig/profiles/default/config/eslint.config.json'], + parserOptions: { + project: './tsconfig.json' + } +} \ No newline at end of file diff --git a/plugins/view/.npmignore b/plugins/view/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/plugins/view/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/plugins/view/config/rig.json b/plugins/view/config/rig.json new file mode 100644 index 0000000000..af1257a896 --- /dev/null +++ b/plugins/view/config/rig.json @@ -0,0 +1,18 @@ +// The "rig.json" file directs tools to look for their config files in an external package. +// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + + /** + * (Required) The name of the rig package to inherit from. + * It should be an NPM package name with the "-rig" suffix. + */ + "rigPackageName": "@anticrm/platform-rig" + + /** + * (Optional) Selects a config profile from the rig package. The name must consist of + * lowercase alphanumeric words separated by hyphens, for example "sample-profile". + * If omitted, then the "default" profile will be used." + */ + // "rigProfile": "your-profile-name" +} diff --git a/plugins/view/package.json b/plugins/view/package.json new file mode 100644 index 0000000000..52b0f010e8 --- /dev/null +++ b/plugins/view/package.json @@ -0,0 +1,25 @@ +{ + "name": "@anticrm/view", + "version": "0.6.0", + "main": "lib/index.js", + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "heft build", + "lint:fix": "eslint --fix src" + }, + "devDependencies": { + "@anticrm/platform-rig":"~0.6.0", + "@types/heft-jest":"^1.0.2", + "@typescript-eslint/eslint-plugin":"4", + "eslint-plugin-import":"2", + "eslint-plugin-promise":"4", + "eslint-plugin-node":"11", + "eslint":"^7.32.0" + }, + "dependencies": { + "@anticrm/platform":"~0.6.3", + "@anticrm/core":"~0.6.0", + "@anticrm/ui":"~0.6.0" + } +} diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts new file mode 100644 index 0000000000..dc186b0626 --- /dev/null +++ b/plugins/view/src/index.ts @@ -0,0 +1,60 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 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 { Plugin, Asset } from '@anticrm/platform' +import { plugin } from '@anticrm/platform' +import type { Ref, Mixin, UXObject, Space, FindOptions, Class, Doc } from '@anticrm/core' + +import type { AnyComponent } from '@anticrm/ui' + +export interface AttributeEditor extends Class { + editor: AnyComponent +} + +export interface AttributePresenter extends Class { + presenter: AnyComponent +} + +export interface ViewletDescriptor extends Doc, UXObject { + component: AnyComponent +} + +export interface Viewlet extends Doc { + attachTo: Ref> + descriptor: Ref + open: AnyComponent + options?: FindOptions + config: any +} + +export const viewId = 'view' as Plugin + +export default plugin(viewId, { + mixin: { + AttributeEditor: '' as Ref>, + AttributePresenter: '' as Ref> + }, + class: { + ViewletDescriptor: '' as Ref>, + Viewlet: '' as Ref> + }, + viewlet: { + Table: '' as Ref + }, + icon: { + Table: '' as Asset + } +}) diff --git a/plugins/view/tsconfig.json b/plugins/view/tsconfig.json new file mode 100644 index 0000000000..32045300ce --- /dev/null +++ b/plugins/view/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./node_modules/@anticrm/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "lib": ["esnext", "dom"] + } +} \ No newline at end of file diff --git a/plugins/workbench-resources/img/avatar.png b/plugins/workbench-resources/img/avatar.png new file mode 100644 index 0000000000..8d61750a2c Binary files /dev/null and b/plugins/workbench-resources/img/avatar.png differ diff --git a/plugins/workbench-resources/img/voltron.png b/plugins/workbench-resources/img/voltron.png new file mode 100644 index 0000000000..b6d498da92 Binary files /dev/null and b/plugins/workbench-resources/img/voltron.png differ diff --git a/plugins/workbench-resources/package.json b/plugins/workbench-resources/package.json index e0a7c7ec36..721a094b5f 100644 --- a/plugins/workbench-resources/package.json +++ b/plugins/workbench-resources/package.json @@ -22,8 +22,10 @@ "svelte": "^3.37.0", "@anticrm/platform": "~0.6.0", "@anticrm/core": "~0.6.0", + "@anticrm/client": "~0.6.0", "@anticrm/workbench": "~0.6.0", "@anticrm/ui": "~0.6.0", + "@anticrm/view": "~0.6.0", "@anticrm/presentation": "~0.6.0" } } diff --git a/plugins/workbench-resources/src/components/WorkbenchApp.svelte b/plugins/workbench-resources/src/components/WorkbenchApp.svelte index 0640b4ad09..6fce7015c4 100644 --- a/plugins/workbench-resources/src/components/WorkbenchApp.svelte +++ b/plugins/workbench-resources/src/components/WorkbenchApp.svelte @@ -16,12 +16,12 @@