diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index d978f655bc..dc6b2ba02c 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/core': file:./projects/core.tgz '@rush-temp/platform': file:./projects/platform.tgz '@rush-temp/platform-rig': file:./projects/platform-rig.tgz '@rushstack/heft': ^0.35.0 @@ -15,12 +16,14 @@ specifiers: eslint-plugin-node: '11' eslint-plugin-promise: '4' intl-messageformat: ^9.7.1 + simplytyped: ^3.3.0 typescript: ^4.3.5 dependencies: '@microsoft/api-extractor': 7.18.4 + '@rush-temp/core': file:projects/core.tgz '@rush-temp/platform': file:projects/platform.tgz - '@rush-temp/platform-rig': file:projects/platform-rig.tgz + '@rush-temp/platform-rig': file:projects/platform-rig.tgz_eslint@7.32.0+typescript@4.3.5 '@rushstack/heft': 0.35.0 '@rushstack/heft-jest-plugin': 0.1.15_@rushstack+heft@0.35.0 '@types/heft-jest': 1.0.2 @@ -32,6 +35,7 @@ dependencies: eslint-plugin-node: 11.1.0_eslint@7.32.0 eslint-plugin-promise: 4.3.1 intl-messageformat: 9.8.1 + simplytyped: 3.3.0_typescript@4.3.5 typescript: 4.3.5 packages: @@ -4452,6 +4456,14 @@ packages: resolution: {integrity: sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==} dev: false + /simplytyped/3.3.0_typescript@4.3.5: + resolution: {integrity: sha512-mz4RaNdKTZiaKXgi6P1k/cdsxV3gz+y1Wh2NXHWD40dExktLh4Xx/h6MFakmQWODZHj/2rKe59acacpL74ZhQA==} + peerDependencies: + typescript: '>=2.8.0' + dependencies: + typescript: 4.3.5 + dev: false + /slash/3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -5154,18 +5166,46 @@ packages: commander: 2.20.3 dev: false - file:projects/platform-rig.tgz: - resolution: {integrity: sha512-bUPKt7uRIPzuShO2D4ncHYn9lqkg7S5IyQkzdL3O06sT027+wkP8SlGdBWPumU/GiPOOB6WIeG4M1/eCjpZvEA==, tarball: file:projects/platform-rig.tgz} + file:projects/core.tgz: + resolution: {integrity: sha512-t+dgsEkYQSoo/p/wy3ZsCCCL5beZ2p7shTsTSAcvssigjlTIBoCKjnukzHLTAiVdRMwxsObAIf8GXEmtRteyvg==, tarball: file:projects/core.tgz} + name: '@rush-temp/core' + version: 0.0.0 + dependencies: + '@types/heft-jest': 1.0.2 + '@typescript-eslint/eslint-plugin': 4.28.5_a8e83fcad666e1ba86be4b2e27a20aea + '@typescript-eslint/parser': 4.28.5_eslint@7.32.0+typescript@4.3.5 + eslint: 7.32.0 + eslint-config-standard-with-typescript: 20.0.0_2e482f375e273d762fe67cbd5e194b49 + 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 + typescript: 4.3.5 + transitivePeerDependencies: + - supports-color + dev: false + + file:projects/platform-rig.tgz_eslint@7.32.0+typescript@4.3.5: + resolution: {integrity: sha512-TKHuAhvUIcQRtvjoXmexJXdGD9HFljUddiThWwSleLTosIPsXNSUr+88F4V4a1pz/HQV1KpiPchn5HxTXFZ8GA==, tarball: file:projects/platform-rig.tgz} + id: file:projects/platform-rig.tgz name: '@rush-temp/platform-rig' version: 0.0.0 dependencies: '@microsoft/api-extractor': 7.18.4 '@rushstack/heft': 0.35.0 '@rushstack/heft-jest-plugin': 0.1.15_@rushstack+heft@0.35.0 + '@typescript-eslint/eslint-plugin': 4.28.5_a8e83fcad666e1ba86be4b2e27a20aea + '@typescript-eslint/parser': 4.28.5_eslint@7.32.0+typescript@4.3.5 + eslint-config-standard-with-typescript: 20.0.0_2e482f375e273d762fe67cbd5e194b49 + 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: - bufferutil - canvas + - eslint - supports-color + - typescript - utf-8-validate dev: false diff --git a/packages/core/.eslintrc.js b/packages/core/.eslintrc.js new file mode 100644 index 0000000000..89f8151bd4 --- /dev/null +++ b/packages/core/.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/core/config/rig.json b/packages/core/config/rig.json new file mode 100644 index 0000000000..af1257a896 --- /dev/null +++ b/packages/core/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/core/package.json b/packages/core/package.json new file mode 100644 index 0000000000..06a84ebc78 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,32 @@ +{ + "name": "@anticrm/core", + "version": "0.5.0", + "main": "lib/index.js", + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "heft build", + "build:docs": "api-extractor run --local", + "test": "jest", + "lint": "ts-standard src", + "lint:fix": "eslint --fix ./src", + "format": "prettier --write 'src/**/*.{ts*,js*,yml}' && ts-standard --fix src" + }, + "devDependencies": { + "@anticrm/platform-rig":"~0.5.0", + "@types/heft-jest":"^1.0.2", + "eslint-plugin-promise":"4", + "eslint-plugin-import":"2", + "eslint-plugin-node":"11", + "@typescript-eslint/eslint-plugin":"4", + "typescript":"^4.3.5", + "eslint":"^7.32.0" + }, + "peerDependencies": { + "@typescript-eslint/parser":"^4.0.0" + }, + "dependencies": { + "@anticrm/platform": "~0.5.0", + "simplytyped": "^3.3.0" + } +} diff --git a/packages/core/src/__tests__/client.test.ts b/packages/core/src/__tests__/client.test.ts new file mode 100644 index 0000000000..799659e2b0 --- /dev/null +++ b/packages/core/src/__tests__/client.test.ts @@ -0,0 +1,43 @@ +// +// 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 { Space } from '../classes' +import { createClient } from '../client' +import core from '../component' +import { withOperations } from '../tx' +import { connect } from './connection' + +describe('client', () => { + it('client', async () => { + const klass = core.class.Space + const client = withOperations(core.account.System, await createClient(connect)) + const result = await client.findAll(klass, {}) + expect(result).toHaveLength(2) + + await client.createDoc(klass, core.space.Model, { + private: false, + name: 'NewSpace', + description: '', + members: [] + }) + const result2 = await client.findAll(klass, {}) + expect(result2).toHaveLength(3) + + await client.createDoc(klass, core.space.Model, { private: false, name: 'NewSpace', description: '', members: [] }) + const result3 = await client.findAll(klass, {}) + expect(result3).toHaveLength(4) + }) +}) diff --git a/packages/core/src/__tests__/connection.ts b/packages/core/src/__tests__/connection.ts new file mode 100644 index 0000000000..4c2d883e08 --- /dev/null +++ b/packages/core/src/__tests__/connection.ts @@ -0,0 +1,53 @@ +// +// 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 { Storage, DocumentQuery, FindResult } from '../storage' +import type { Class, Doc, Ref, Tx } from '../classes' +import core from '../component' +import { Hierarchy } from '../hierarchy' +import { ModelDb, TxDb } from '../memdb' +import { DOMAIN_TX } from '../tx' +import { genMinModel } from './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/core/src/__tests__/hierarchy.test.ts b/packages/core/src/__tests__/hierarchy.test.ts new file mode 100644 index 0000000000..3b916bbfc7 --- /dev/null +++ b/packages/core/src/__tests__/hierarchy.test.ts @@ -0,0 +1,57 @@ +// +// 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 { Class, Doc, Obj, Ref, TxCreateDoc } from '../classes' +import core from '../component' +import { Hierarchy } from '../hierarchy' +import { genMinModel } from './minmodel' + +const txes = genMinModel() + +describe('hierarchy', () => { + it('should build hierarchy', async () => { + const hierarchy = new Hierarchy() + for (const tx of txes) hierarchy.tx(tx) + const ancestors = hierarchy.getAncestors(core.class.TxCreateDoc) + expect(ancestors).toContain(core.class.Tx) + }) + + it('isDerived', async () => { + const hierarchy = new Hierarchy() + for (const tx of txes) hierarchy.tx(tx) + const derived = hierarchy.isDerived(core.class.Space, core.class.Doc) + expect(derived).toBeTruthy() + const notDerived = hierarchy.isDerived(core.class.Space, core.class.Class) + expect(notDerived).not.toBeTruthy() + }) + + it('getClass', async () => { + const hierarchy = new Hierarchy() + for (const tx of txes) hierarchy.tx(tx) + const data = hierarchy.getClass(core.class.TxCreateDoc) + expect(data).toMatchObject((txes.find((p) => p.objectId === core.class.TxCreateDoc) as TxCreateDoc).attributes) + const notExistClass = 'class:test.MyClass' as Ref> + expect(() => hierarchy.getClass(notExistClass)).toThrowError('class not found: ' + notExistClass) + }) + + it('getDomain', async () => { + const hierarchy = new Hierarchy() + for (const tx of txes) hierarchy.tx(tx) + const txDomain = hierarchy.getDomain(core.class.TxCreateDoc) + expect(txDomain).toBe('tx') + const modelDomain = hierarchy.getDomain(core.class.Class) + expect(modelDomain).toBe('model') + }) +}) diff --git a/packages/core/src/__tests__/memdb.test.ts b/packages/core/src/__tests__/memdb.test.ts new file mode 100644 index 0000000000..dc2e72ac89 --- /dev/null +++ b/packages/core/src/__tests__/memdb.test.ts @@ -0,0 +1,194 @@ +// +// 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 { Class, Doc, Obj, Ref } from '../classes' +import core from '../component' +import { Hierarchy } from '../hierarchy' +import { ModelDb, TxDb } from '../memdb' +import { SortingOrder } from '../storage' +import { withOperations } from '../tx' +import { genMinModel } from './minmodel' + +const txes = genMinModel() + +describe('memdb', () => { + it('should save all tx', async () => { + const hierarchy = new Hierarchy() + for (const tx of txes) hierarchy.tx(tx) + const txDb = new TxDb(hierarchy) + for (const tx of txes) await txDb.tx(tx) + const result = await txDb.findAll(core.class.Tx, {}) + expect(result.length).toBe(txes.filter((tx) => tx._class === core.class.TxCreateDoc).length) + }) + + it('should query model', async () => { + const hierarchy = new Hierarchy() + for (const tx of txes) await hierarchy.tx(tx) + const model = new ModelDb(hierarchy) + for (const tx of txes) await model.tx(tx) + const result = await model.findAll(core.class.Class, {}) + expect(result.length).toBeGreaterThan(5) + const result2 = await model.findAll('class:workbench.Application' as Ref>, { _id: undefined }) + expect(result2).toHaveLength(0) + }) + + it('should allow delete', async () => { + const hierarchy = new Hierarchy() + for (const tx of txes) await hierarchy.tx(tx) + const model = new ModelDb(hierarchy) + for (const tx of txes) await model.tx(tx) + const result = await model.findAll(core.class.Space, {}) + expect(result.length).toBe(2) + + const ops = withOperations(core.account.System, model) + await ops.removeDoc(result[0]._class, result[0].space, result[0]._id) + const result2 = await model.findAll(core.class.Space, {}) + expect(result2).toHaveLength(1) + }) + + it('should query model with params', async () => { + const hierarchy = new Hierarchy() + for (const tx of txes) hierarchy.tx(tx) + const model = new ModelDb(hierarchy) + for (const tx of txes) await model.tx(tx) + const first = await model.findAll(core.class.Class, { + _id: txes[1].objectId as Ref>, + kind: 0 + }) + expect(first.length).toBe(1) + const second = await model.findAll(core.class.Class, { + _id: { $in: [txes[1].objectId as Ref>, txes[3].objectId as Ref>] } + }) + expect(second.length).toBe(2) + const incorrectId = await model.findAll(core.class.Class, { + _id: (txes[1].objectId + 'test') as Ref> + }) + expect(incorrectId.length).toBe(0) + const result = await model.findAll(core.class.Class, { + _id: txes[1].objectId as Ref>, + kind: 1 + }) + expect(result.length).toBe(0) + const multipleParam = await model.findAll(core.class.Doc, { + space: { $in: [core.space.Model, core.space.Tx] } + }) + expect(multipleParam.length).toBeGreaterThan(5) + }) + + it('should query model like params', async () => { + const hierarchy = new Hierarchy() + for (const tx of txes) hierarchy.tx(tx) + const model = new ModelDb(hierarchy) + for (const tx of txes) await model.tx(tx) + const expectedLength = txes.filter((tx) => tx.objectSpace === core.space.Model).length + const without = await model.findAll(core.class.Doc, { + space: { $like: core.space.Model } + }) + expect(without).toHaveLength(expectedLength) + const begin = await model.findAll(core.class.Doc, { + space: { $like: '%Model' } + }) + expect(begin).toHaveLength(expectedLength) + const zero = await model.findAll(core.class.Doc, { + space: { $like: 'Model' } + }) + expect(zero).toHaveLength(0) + const end = await model.findAll(core.class.Doc, { + space: { $like: 'core.space.M%' } + }) + expect(end).toHaveLength(expectedLength) + const mid = await model.findAll(core.class.Doc, { + space: { $like: '%M%de%' } + }) + expect(mid).toHaveLength(expectedLength) + const all = await model.findAll(core.class.Doc, { + space: { $like: '%Mod%' } + }) + expect(all).toHaveLength(expectedLength) + }) + + it('should push to array', async () => { + const hierarchy = new Hierarchy() + for (const tx of txes) await hierarchy.tx(tx) + const model = withOperations(core.account.System, new ModelDb(hierarchy)) + for (const tx of txes) await model.tx(tx) + const space = await model.createDoc(core.class.Space, core.space.Model, { + name: 'name', + description: 'desc', + private: false, + members: [] + }) + const account = await model.createDoc(core.class.Account, core.space.Model, {}) + await model.updateDoc(core.class.Space, core.space.Model, space._id, { $push: { members: account._id } }) + const txSpace = await model.findAll(core.class.Space, { _id: space._id }) + expect(txSpace[0].members).toEqual(expect.arrayContaining([account._id])) + }) + + it('limit and sorting', async () => { + const hierarchy = new Hierarchy() + for (const tx of txes) hierarchy.tx(tx) + const model = withOperations(core.account.System, new ModelDb(hierarchy)) + for (const tx of txes) await model.tx(tx) + + const without = await model.findAll(core.class.Space, {}) + expect(without).toHaveLength(2) + + const limit = await model.findAll(core.class.Space, {}, { limit: 1 }) + expect(limit).toHaveLength(1) + + const sortAsc = await model.findAll(core.class.Space, {}, { limit: 1, sort: { name: SortingOrder.Ascending } }) + expect(sortAsc[0].name).toMatch('Sp1') + + const sortDesc = await model.findAll(core.class.Space, {}, { limit: 1, sort: { name: SortingOrder.Descending } }) + expect(sortDesc[0].name).toMatch('Sp2') + + const numberSortDesc = await model.findAll(core.class.Doc, {}, { sort: { modifiedOn: SortingOrder.Descending } }) + expect(numberSortDesc[0].modifiedOn).toBeGreaterThanOrEqual(numberSortDesc[numberSortDesc.length - 1].modifiedOn) + + const numberSort = await model.findAll(core.class.Doc, {}, { sort: { modifiedOn: SortingOrder.Ascending } }) + expect(numberSort[0].modifiedOn).toBeLessThanOrEqual(numberSort[numberSortDesc.length - 1].modifiedOn) + }) + + // it('should throw error', async () => { + // expect.assertions(1) + // const errorTx: TxAddCollection = { + // _id: '60b73133d22498e666800cd2' as Ref>, + // _class: 'class:core.TxAddCollection' as Ref>>, + // space: core.space.Tx, + // modifiedBy: 'xxx' as Ref, + // modifiedOn: 0, + // objectId: 'class:test.MyClass' as Ref, + // objectSpace: core.space.Model, + // itemClass: 'class:core.Attribute' as Ref>, + // collection: 'attributes', + // localId: 'name', + // attributes: { + // _class: 'class:core.Attribute' as Ref>, + // // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + // __embedded: { + // _class: 'class:core.Attribute' as Ref> + // } as Emb + // } + // } + + // const hierarchy = new Hierarchy() + // for (const tx of txes) hierarchy.tx(tx) + // const model = new ModelDb(hierarchy) + + // await model.tx(errorTx).catch((error: Error) => { + // expect(error.message).toBe('ERROR: status:core.ObjectNotFound') + // }) + // }) +}) diff --git a/packages/core/src/__tests__/minmodel.ts b/packages/core/src/__tests__/minmodel.ts new file mode 100644 index 0000000000..e21e444b61 --- /dev/null +++ b/packages/core/src/__tests__/minmodel.ts @@ -0,0 +1,76 @@ +import { Account, Class, ClassifierKind, Data, Doc, Domain, DOMAIN_MODEL, Obj, Ref, Tx, TxCreateDoc } from '../classes' +import core from '../component' +import { DOMAIN_TX } from '../tx' +import { generateId } from '../utils' + +export function createClass> (_id: Ref, cl: Omit, 'kind'>, domain?: Domain): Tx { + const result: TxCreateDoc = { + _id: generateId(), + _class: core.class.TxCreateDoc, + objectId: _id, + objectClass: core.class.Class, + attributes: { + kind: ClassifierKind.CLASS, + domain: domain ?? DOMAIN_MODEL, + ...cl + }, + modifiedBy: 'model' as Ref, + modifiedOn: Date.now(), + objectSpace: core.space.Model, + space: core.space.Model + } + return result +} + +export function createDoc (_class: Ref>, attributes: Data): Tx { + const tx: TxCreateDoc = { + _id: generateId(), + _class: core.class.TxCreateDoc, + space: core.space.Tx, + modifiedBy: core.account.System, + modifiedOn: Date.now(), + objectId: generateId(), + objectClass: _class, + objectSpace: core.space.Model, + attributes + } + return tx +} + +/** + * Generate minimal model for testing purposes. + * @returns R + */ +export function genMinModel (): Tx[] { + const txes = [] + // Fill Tx'es with basic model classes. + txes.push(createClass(core.class.Obj, {})) + txes.push(createClass(core.class.Doc, { extends: core.class.Obj })) + txes.push(createClass(core.class.Class, { extends: core.class.Doc })) + txes.push(createClass(core.class.Space, { extends: core.class.Doc })) + txes.push(createClass(core.class.Account, { extends: core.class.Doc })) + + txes.push(createClass(core.class.Tx, { extends: core.class.Doc }, DOMAIN_TX)) + txes.push(createClass(core.class.TxCreateDoc, { extends: core.class.Tx }, DOMAIN_TX)) + txes.push(createClass(core.class.TxUpdateDoc, { extends: core.class.Tx }, DOMAIN_TX)) + txes.push(createClass(core.class.TxRemoveDoc, { extends: core.class.Tx }, DOMAIN_TX)) + + txes.push( + createDoc(core.class.Space, { + name: 'Sp1', + description: '', + private: false, + members: [] + }) + ) + + txes.push( + createDoc(core.class.Space, { + name: 'Sp2', + description: '', + private: false, + members: [] + }) + ) + return txes +} diff --git a/packages/core/src/classes.ts b/packages/core/src/classes.ts new file mode 100644 index 0000000000..49e51a619e --- /dev/null +++ b/packages/core/src/classes.ts @@ -0,0 +1,130 @@ +// +// 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 type { IntlString, Asset, Resource } from '@anticrm/platform' + +export type Ref = string & { __ref: T } +export type PrimitiveType = number | string | boolean | undefined | Ref +export type Timestamp = number + +export interface Obj { + _class: Ref> +} + +export interface Doc extends Obj { + _id: Ref + space: Ref + modifiedOn: Timestamp + modifiedBy: Ref +} + +export type PropertyType = any + +export interface UXObject extends Obj { + label?: IntlString + icon?: Asset +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export interface Type extends UXObject {} + +export interface Attribute extends Doc, UXObject { + attributeOf: Ref> + name: string + type: Type +} + +export type AnyAttribute = Attribute> + +export enum ClassifierKind { + CLASS, + MIXIN +} + +export interface Classifier extends Doc, UXObject { + kind: ClassifierKind +} + +export type Domain = string & { __domain: true } + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export interface Class extends Classifier { + extends?: Ref> + domain?: Domain + triggers?: Trigger[] +} + +export type Mixin = Class + +// D A T A + +export type Data = Omit + +// T Y P E S + +export interface RefTo extends Type>> { + to: Ref> +} + +export type Bag = Record + +export interface BagOf extends Type> { + of: Type +} + +export type Arr = T[] + +export interface ArrOf extends Type { + of: Type +} + +export const DOMAIN_MODEL = 'model' as Domain + +// S E C U R I T Y + +export interface Space extends Doc { + name: string + description: string + private: boolean + members: Arr> +} + +export interface Account extends Doc {} + +// T X + +export interface TxFactory { + createTxCreateDoc: (_class: Ref>, space: Ref, attributes: Data) => TxCreateDoc + createTxMixin: (objectId: Ref, objectClass: Ref>, mixin: Ref>, attributes: ExtendedAttributes) => TxMixin +} + +export type Trigger = Resource<(tx: Tx, txFactory: TxFactory) => Promise> + +export interface Tx extends Doc { + objectId: Ref + objectClass: Ref> + objectSpace: Ref +} + +export interface TxCreateDoc extends Tx { + attributes: Data +} + +export type ExtendedAttributes = Omit + +export interface TxMixin extends Tx { + mixin: Ref> + attributes: ExtendedAttributes +} diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts new file mode 100644 index 0000000000..603cae717f --- /dev/null +++ b/packages/core/src/client.ts @@ -0,0 +1,95 @@ +// +// 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 { Doc, Ref, Class, Tx } from './classes' +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' + +type TxHander = (tx: Tx) => void + +export interface Client extends Storage { + getHierarchy: () => Hierarchy +} + +class ClientImpl extends TxProcessor implements Storage, Client { + constructor (private readonly hierarchy: Hierarchy, private readonly model: ModelDb, private readonly conn: Storage) { + super() + } + + getHierarchy (): Hierarchy { return this.hierarchy } + + async findAll( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise> { + const clazz = this.hierarchy.getClass(_class) + if (clazz.domain === DOMAIN_MODEL) { + return await this.model.findAll(_class, query, options) + } + return await this.conn.findAll(_class, query, options) + } + + async tx (tx: Tx): Promise { + await this.conn.tx(tx) + } +} + +export async function createClient ( + connect: (txHandler: TxHander) => Promise, + notify?: (tx: Tx) => void +): Promise { + let client: Client | null = null + let txBuffer: Tx[] | undefined = [] + + const hierarchy = new Hierarchy() + const model = new ModelDb(hierarchy) + + function txHander (tx: Tx): void { + if (client === null) { + txBuffer?.push(tx) + } else { + if (tx.objectSpace === core.space.Model) { + hierarchy.tx(tx) + // eslint-disable-next-line @typescript-eslint/no-floating-promises + model.tx(tx) + } + notify?.(tx) + } + } + + const conn = await connect(txHander) + const txes = await conn.findAll(core.class.Tx, { objectSpace: core.space.Model }) + + const txMap = new Map, Ref>() + for (const tx of txes) txMap.set(tx._id, tx._id) + for (const tx of txes) hierarchy.tx(tx) + for (const tx of txes) await model.tx(tx) + + txBuffer = txBuffer.filter((tx) => txMap.get(tx._id) === undefined) + + client = new ClientImpl(hierarchy, model, conn) + + for (const tx of txBuffer) txHander(tx) + txBuffer = undefined + + return client +} diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts new file mode 100644 index 0000000000..80e54a625b --- /dev/null +++ b/packages/core/src/component.ts @@ -0,0 +1,47 @@ +// +// 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 type { Plugin, StatusCode } from '@anticrm/platform' +import { plugin } from '@anticrm/platform' +import type { Account, Class, Doc, Obj, Ref, Space, AnyAttribute, Tx, TxCreateDoc, TxMixin } from './classes' +import type { TxRemoveDoc, TxUpdateDoc } from './tx' + +export const coreId = 'core' as Plugin + +export default plugin(coreId, { + class: { + Obj: '' as Ref>, + Doc: '' as Ref>, + Class: '' as Ref>>, + Attribute: '' as Ref>, + Tx: '' as Ref>, + TxCreateDoc: '' as Ref>>, + TxMixin: '' as Ref>>, + TxUpdateDoc: '' as Ref>>, + TxRemoveDoc: '' as Ref>>, + Space: '' as Ref>, + Account: '' as Ref> + }, + space: { + Tx: '' as Ref, + Model: '' as Ref + }, + account: { + System: '' as Ref + }, + status: { + ObjectNotFound: '' as StatusCode<{ _id: Ref }>, + ItemNotFound: '' as StatusCode<{ _id: Ref, _localId: string }> + } +}) diff --git a/packages/core/src/hierarchy.ts b/packages/core/src/hierarchy.ts new file mode 100644 index 0000000000..d0bd05c9bc --- /dev/null +++ b/packages/core/src/hierarchy.ts @@ -0,0 +1,175 @@ +// +// 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 type { Ref, Class, Obj, Domain, Mixin, Doc, AnyAttribute, Tx, TxCreateDoc, TxMixin } from './classes' +import { ClassifierKind } from './classes' +import { TxProcessor } from './tx' + +import core from './component' + +export class Hierarchy { + private readonly classes = new Map>, Class>() + private readonly attributes = new Map>, Map>() + private readonly descendants = new Map>, Ref>[]>() + private readonly ancestors = new Map>, Ref>[]>() + private readonly proxies = new Map>, ProxyHandler>() + + private createMixinProxyHandler (mixin: Ref>): ProxyHandler { + const value = this.getClass(mixin) + const ancestor = this.getClass(value.extends as Ref>) + const ancestorProxy = ancestor.kind === ClassifierKind.MIXIN ? this.getMixinProxyHandler(ancestor._id) : null + return { + get (target: any, property: string, receiver: any): any { + const value = target[mixin]?.[property] + if (value === undefined) { return (ancestorProxy !== null) ? ancestorProxy.get?.(target, property, receiver) : target[property] } + return value + } + } + } + + private getMixinProxyHandler (mixin: Ref>): ProxyHandler { + const handler = this.proxies.get(mixin) + if (handler === undefined) { + const handler = this.createMixinProxyHandler(mixin) + this.proxies.set(mixin, handler) + return handler + } + return handler + } + + as(doc: D, mixin: Ref>): M { + return new Proxy(doc, this.getMixinProxyHandler(mixin)) as M + } + + getAncestors (_class: Ref>): Ref>[] { + const result = this.ancestors.get(_class) + if (result === undefined) { + throw new Error('ancestors not found: ' + _class) + } + return result + } + + getClass (_class: Ref>): Class { + const data = this.classes.get(_class) + if (data === undefined) { + throw new Error('class not found: ' + _class) + } + return data + } + + getDomain (_class: Ref>): Domain { + const klazz = this.getClass(_class) + if (klazz.domain !== undefined) { + return klazz.domain + } + if (klazz.extends !== undefined) { + const domain = this.getDomain(klazz.extends) + klazz.domain = domain + return domain + } + throw new Error('domain not found: ' + _class) + } + + tx (tx: Tx): void { + if (tx._class !== core.class.TxCreateDoc) { + if (tx._class === core.class.TxMixin) { + const mixinTx = tx as TxMixin + if (tx.objectClass !== core.class.Class) { return } + const obj = this.getClass(tx.objectId as Ref>) as any + obj[mixinTx.mixin] = mixinTx.attributes + } + return + } + const createTx = tx as TxCreateDoc + if (createTx.objectClass === core.class.Class) { + const createTx = tx as TxCreateDoc> + const _id = createTx.objectId + this.classes.set(_id, TxProcessor.createDoc2Doc(createTx)) + this.addAncestors(_id) + this.addDescendant(_id) + } else if (createTx.objectClass === core.class.Attribute) { + const createTx = tx as TxCreateDoc + this.addAttribute(TxProcessor.createDoc2Doc(createTx)) + } + } + + isDerived(_class: Ref>, from: Ref>): boolean { + let cl: Ref> | undefined = _class + while (cl !== undefined) { + if (cl === from) return true + const attrs = this.classes.get(cl) + cl = attrs?.extends + } + return false + } + + getDescendants(_class: Ref>): Ref>[] { + const data = this.descendants.get(_class) + if (data === undefined) { + throw new Error('descendants not found: ' + _class) + } + return data + } + + private addDescendant(_class: Ref>): void { + const hierarchy = this.getAncestors(_class) + for (const cls of hierarchy) { + const list = this.descendants.get(cls) + if (list === undefined) { + this.descendants.set(cls, [_class]) + } else { + list.push(_class) + } + } + } + + private addAncestors(_class: Ref>): void { + let cl: Ref> | undefined = _class + while (cl !== undefined) { + const list = this.ancestors.get(_class) + if (list === undefined) { + this.ancestors.set(_class, [cl]) + } else { + list.push(cl) + } + const attrs = this.classes.get(cl) + cl = attrs?.extends + } + } + + private addAttribute (attribute: AnyAttribute): void { + const _class = attribute.attributeOf + let attributes = this.attributes.get(_class) + if (attributes === undefined) { + attributes = new Map() + this.attributes.set(_class, attributes) + } + attributes.set(attribute.name, attribute) + console.log('added attribute:', _class, attribute.name) + } + + getAttribute (_class: Ref>, name: string): AnyAttribute { + const attribute = this.attributes.get(_class)?.get(name) + if (attribute === undefined) { + const clazz = this.getClass(_class) + if (clazz.extends !== undefined) { + return this.getAttribute(clazz.extends, name) + } else { + throw new Error('attribute not found: ' + name) + } + } + return attribute + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000000..b6b5bd365f --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,25 @@ +// +// 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. +// +export * from './classes' +export * from './tx' +export * from './storage' +export * from './utils' +export * from './hierarchy' +export * from './memdb' +export * from './client' +export * from './operator' +export * from './query' + +export { default, coreId } from './component' diff --git a/packages/core/src/memdb.ts b/packages/core/src/memdb.ts new file mode 100644 index 0000000000..bfd5fcee61 --- /dev/null +++ b/packages/core/src/memdb.ts @@ -0,0 +1,180 @@ +// +// 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 { PlatformError, Severity, Status } from '@anticrm/platform' +import type { Class, Doc, Ref, Tx, TxCreateDoc, TxMixin } from './classes' +import core from './component' +import type { Hierarchy } from './hierarchy' +import { getOperator } from './operator' +import { findProperty, resultSort } from './query' +import type { DocumentQuery, FindOptions, FindResult, Storage, WithLookup, LookupData, Refs } from './storage' +import type { TxRemoveDoc, TxUpdateDoc } from './tx' +import { TxProcessor } from './tx' + +class MemDb extends TxProcessor { + protected readonly hierarchy: Hierarchy + private readonly objectsByClass = new Map>, Doc[]>() + private readonly objectById = new Map, Doc>() + + constructor (hierarchy: Hierarchy) { + super() + this.hierarchy = hierarchy + } + + private getObjectsByClass (_class: Ref>): Doc[] { + const result = this.objectsByClass.get(_class) + if (result === undefined) { + const result: Doc[] = [] + this.objectsByClass.set(_class, result) + return result + } + return result + } + + private cleanObjectByClass (_class: Ref>, _id: Ref): void { + let result = this.objectsByClass.get(_class) + if (result !== undefined) { + result = result.filter((cl) => cl._id !== _id) + this.objectsByClass.set(_class, result) + } + } + + private getByIdQuery(query: DocumentQuery, _class: Ref>): T[] { + const result: T[] = [] + if (typeof query._id === 'string') { + const obj = this.objectById.get(query._id) as T + if (obj !== undefined) result.push(obj) + } else if (query._id?.$in !== undefined) { + const ids = query._id.$in + for (const id of ids) { + const obj = this.objectById.get(id) as T + if (obj !== undefined) result.push(obj) + } + } + return result + } + + getObject(_id: Ref): T { + const doc = this.objectById.get(_id) + if (doc === undefined) { + console.log(_id) + throw new PlatformError(new Status(Severity.ERROR, core.status.ObjectNotFound, { _id })) + } + return doc as T + } + + private lookup(docs: T[], lookup: Refs): WithLookup[] { + const withLookup: WithLookup[] = [] + for (const doc of docs) { + const result: LookupData = {} + for (const key in lookup) { + const id = (doc as any)[key] as Ref + (result as any)[key] = this.getObject(id) + } + withLookup.push(Object.assign({}, doc, { $lookup: result })) + } + return withLookup + } + + async findAll( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise> { + let result: Doc[] + if ( + Object.prototype.hasOwnProperty.call(query, '_id') && + (typeof query._id === 'string' || query._id?.$in !== undefined || query._id === undefined) + ) { + result = this.getByIdQuery(query, _class) + } else { + result = this.getObjectsByClass(_class) + } + + for (const key in query) { + if (key === '_id' && ((query._id as any)?.$like === undefined || query._id === undefined)) continue + const value = (query as any)[key] + result = findProperty(result, key, value) + } + + if (options?.sort !== undefined) resultSort(result, options?.sort) + + if (options?.lookup !== undefined) result = this.lookup(result as T[], options.lookup) + + result = result.slice(0, options?.limit) + return result as T[] + } + + addDoc (doc: Doc): void { + this.hierarchy.getAncestors(doc._class).forEach((_class) => { + this.getObjectsByClass(_class).push(doc) + }) + this.objectById.set(doc._id, doc) + } + + delDoc (_id: Ref): void { + const doc = this.objectById.get(_id) + if (doc === undefined) { + throw new PlatformError(new Status(Severity.ERROR, core.status.ObjectNotFound, { _id })) + } + this.objectById.delete(_id) + this.hierarchy.getAncestors(doc._class).forEach((_class) => { + this.cleanObjectByClass(_class, _id) + }) + } +} + +/** + * Hold transactions + */ +export class TxDb extends MemDb implements Storage { + async tx (tx: Tx): Promise { + this.addDoc(tx) + } +} + +/** + * Hold model objects and classes + */ +export class ModelDb extends MemDb implements Storage { + protected async txCreateDoc (tx: TxCreateDoc): Promise { + this.addDoc(TxProcessor.createDoc2Doc(tx)) + } + + protected async txUpdateDoc (tx: TxUpdateDoc): Promise { + const doc = this.getObject(tx.objectId) as any + const ops = tx.operations as any + for (const key in ops) { + if (key.startsWith('$')) { + const operator = getOperator(key) + operator(doc, ops[key]) + } else { + doc[key] = ops[key] + } + } + doc.modifiedBy = tx.modifiedBy + doc.modifiedOn = tx.modifiedOn + } + + protected async txRemoveDoc (tx: TxRemoveDoc): Promise { + this.delDoc(tx.objectId) + } + + // TODO: process ancessor mixins + protected async txMixin (tx: TxMixin): Promise { + const obj = this.getObject(tx.objectId) as any + obj[tx.mixin] = tx.attributes + } +} diff --git a/packages/core/src/operator.ts b/packages/core/src/operator.ts new file mode 100644 index 0000000000..7d40eadd10 --- /dev/null +++ b/packages/core/src/operator.ts @@ -0,0 +1,41 @@ +// +// 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 { Doc, PropertyType } from './classes' + +type OperatorFunc = (doc: Doc, op: object) => void + +function $push (document: Doc, keyval: Record): void { + const doc = document as any + for (const key in keyval) { + const arr = doc[key] + if (arr === undefined) { + doc[key] = [keyval[key]] + } else { + arr.push(keyval[key]) + } + } +} + +const operators: Record = { + $push +} + +export function getOperator (name: string): OperatorFunc { + const operator = operators[name] + if (operator === undefined) throw new Error('unknown operator: ' + name) + return operator +} diff --git a/packages/core/src/predicate.ts b/packages/core/src/predicate.ts new file mode 100644 index 0000000000..e08961918f --- /dev/null +++ b/packages/core/src/predicate.ts @@ -0,0 +1,64 @@ +// +// 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 { Doc } from './classes' +import { checkLikeQuery } from './query' + +type Predicate = (docs: Doc[]) => Doc[] +type PredicateFactory = (pred: any, propertyKey: string) => Predicate + +const predicates: Record = { + $in: (o: any, propertyKey: string): Predicate => { + if (!Array.isArray(o)) { + throw new Error('$in predicate requires array') + } + return (docs: Doc[]): Doc[] => { + const result: Doc[] = [] + for (const doc of docs) { + if (o.includes((doc as any)[propertyKey])) result.push(doc) + } + return result + } + }, + + $like: (query: string, propertyKey: string): Predicate => { + return (docs: Doc[]): Doc[] => { + const result: Doc[] = [] + for (const doc of docs) { + const value = (doc as any)[propertyKey] as string + if (checkLikeQuery(value, query)) result.push(doc) + } + return result + } + } +} + +export function isPredicate (o: Record): boolean { + if (typeof o !== 'object') { return false } + const keys = Object.keys(o) + return keys.length > 0 && keys.every((key) => key.startsWith('$')) +} + +export function createPredicates (o: Record, propertyKey: string): Predicate[] { + const keys = Object.keys(o) + const result: Predicate[] = [] + for (const key of keys) { + const factory = predicates[key] + if (factory === undefined) throw new Error('unknown predicate: ' + keys[0]) + result.push(factory(o[key], propertyKey)) + } + return result +} diff --git a/packages/core/src/query.ts b/packages/core/src/query.ts new file mode 100644 index 0000000000..72daf92206 --- /dev/null +++ b/packages/core/src/query.ts @@ -0,0 +1,39 @@ +import { Doc } from './classes' +import { createPredicates, isPredicate } from './predicate' +import { SortingQuery } from './storage' + +export const likeSymbol = '%' + +export function checkLikeQuery (value: string, query: string): boolean { + const searchString = query.split(likeSymbol).join('.*') + const regex = RegExp(`^${searchString}$`) + return regex.test(value) +} + +export function findProperty (objects: Doc[], propertyKey: string, value: any): Doc[] { + if (isPredicate(value)) { + const preds = createPredicates(value, propertyKey) + for (const pred of preds) { + objects = pred(objects) + } + return objects + } + const result: Doc[] = [] + for (const object of objects) { + if ((object as any)[propertyKey] === value) { + result.push(object) + } + } + return result +} + +export function resultSort (result: T[], sortOptions: SortingQuery): void { + const sortFunc = (a: any, b: any): number => { + for (const key in sortOptions) { + const result = typeof a[key] === 'string' ? a[key].localeCompare(b[key]) : a[key] - b[key] + if (result !== 0) return result * (sortOptions[key] as number) + } + return 0 + } + result.sort(sortFunc) +} diff --git a/packages/core/src/storage.ts b/packages/core/src/storage.ts new file mode 100644 index 0000000000..a911aa030f --- /dev/null +++ b/packages/core/src/storage.ts @@ -0,0 +1,70 @@ +// +// 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 { KeysByType } from 'simplytyped' +import type { Class, Doc, Ref, Tx } from './classes' + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type QuerySelector = { + $in?: T[] + $like?: string +} + +export type ObjQueryType = T | QuerySelector + +export type DocumentQuery = { + [P in keyof T]?: ObjQueryType +} + +export type Refs = Partial>>> + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type FindOptions = { + limit?: number + sort?: SortingQuery + lookup?: Refs +} + +export type SortingQuery = { + [P in keyof T]?: T[P] extends object ? never : SortingOrder +} + +export enum SortingOrder { + Ascending = 1, + Descending = -1 +} + +type RefsAsDocs = { + [P in keyof T]: T[P] extends Ref ? X : never +} + +type RemoveNever = Omit> + +export type LookupData = Partial>> + +export type WithLookup = T & { + $lookup?: LookupData +} + +export type FindResult = WithLookup[] + +export interface Storage { + findAll: ( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ) => Promise> + tx: (tx: Tx) => Promise +} diff --git a/packages/core/src/tx.ts b/packages/core/src/tx.ts new file mode 100644 index 0000000000..d6c4882497 --- /dev/null +++ b/packages/core/src/tx.ts @@ -0,0 +1,184 @@ +// +// 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 type { KeysByType } from 'simplytyped' +import type { Class, Data, Doc, Domain, Ref, Account, Space, Arr, Mixin, Tx, TxCreateDoc, TxFactory, TxMixin, ExtendedAttributes } from './classes' +import core from './component' +import { generateId } from './utils' + +type ArrayAsElement = { + [P in keyof T]: T[P] extends Arr ? X : never +} + +type OmitNever = Omit> + +interface PushOptions { + $push?: Partial>> +} + +export type DocumentUpdate = Partial> & PushOptions + +export interface TxUpdateDoc extends Tx { + operations: DocumentUpdate +} + +export interface TxRemoveDoc extends Tx { +} + +export const DOMAIN_TX = 'tx' as Domain + +interface WithTx { + tx: (tx: Tx) => Promise +} + +export class TxProcessor implements WithTx { + async tx (tx: Tx): Promise { + switch (tx._class) { + case core.class.TxCreateDoc: + return await this.txCreateDoc(tx as TxCreateDoc) + case core.class.TxUpdateDoc: + return await this.txUpdateDoc(tx as TxUpdateDoc) + case core.class.TxRemoveDoc: + return await this.txRemoveDoc(tx as TxRemoveDoc) + case core.class.TxMixin: + return await this.txMixin(tx as TxMixin) + } + } + + static createDoc2Doc (tx: TxCreateDoc): T { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return { + _id: tx.objectId, + _class: tx.objectClass, + space: tx.objectSpace, + modifiedBy: tx.modifiedBy, + modifiedOn: tx.modifiedOn, + ...tx.attributes + } as T + } + + protected async txCreateDoc (tx: TxCreateDoc): Promise {} + protected async txUpdateDoc (tx: TxUpdateDoc): Promise {} + protected async txRemoveDoc (tx: TxRemoveDoc): Promise {} + protected async txMixin (tx: TxMixin): Promise {} +} + +export interface TxOperations { + createDoc: (_class: Ref>, space: Ref, attributes: Data) => Promise + updateDoc: ( + _class: Ref>, + space: Ref, + objectId: Ref, + operations: DocumentUpdate + ) => Promise + removeDoc: (_class: Ref>, space: Ref, objectId: Ref) => Promise +} + +export function withOperations (user: Ref, storage: T): T & TxOperations { + const result = storage as T & TxOperations + + result.createDoc = async ( + _class: Ref>, + space: Ref, + attributes: Data + ): Promise => { + const tx: TxCreateDoc = { + _id: generateId(), + _class: core.class.TxCreateDoc, + space: core.space.Tx, + modifiedBy: user, + modifiedOn: Date.now(), + objectId: generateId(), + objectClass: _class, + objectSpace: space, + attributes + } + await storage.tx(tx) + return TxProcessor.createDoc2Doc(tx) + } + + result.updateDoc = async ( + _class: Ref>, + space: Ref, + objectId: Ref, + operations: DocumentUpdate + ): Promise => { + const tx: TxUpdateDoc = { + _id: generateId(), + _class: core.class.TxUpdateDoc, + space: core.space.Tx, + modifiedBy: user, + modifiedOn: Date.now(), + objectId, + objectClass: _class, + objectSpace: space, + operations + } + await storage.tx(tx) + } + + result.removeDoc = async ( + _class: Ref>, + space: Ref, + objectId: Ref + ): Promise => { + const tx: TxRemoveDoc = { + _id: generateId(), + _class: core.class.TxRemoveDoc, + space: core.space.Tx, + modifiedBy: user, + modifiedOn: Date.now(), + objectId, + objectClass: _class, + objectSpace: space + } + await storage.tx(tx) + } + + return result +} + +export class DefaultTxFactory implements TxFactory { + constructor (readonly account: Ref) {} + + createTxCreateDoc(_class: Ref>, space: Ref, attributes: Data, objectId?: Ref): TxCreateDoc { + return { + _id: generateId(), + _class: core.class.TxCreateDoc, + space: core.space.Tx, + objectId: objectId ?? generateId(), + objectClass: _class, + objectSpace: space, + modifiedOn: Date.now(), + modifiedBy: this.account, + attributes + } + } + + createTxMixin(objectId: Ref, objectClass: Ref>, mixin: Ref>, attributes: ExtendedAttributes): TxMixin { + return { + _id: generateId(), + _class: core.class.TxMixin, + space: core.space.Tx, + modifiedBy: this.account, + modifiedOn: Date.now(), + objectId, + objectClass, + objectSpace: core.space.Model, + mixin, + attributes + } + } +} diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts new file mode 100644 index 0000000000..eef2c862ef --- /dev/null +++ b/packages/core/src/utils.ts @@ -0,0 +1,41 @@ +// +// 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 type { Doc, Ref } from './classes' + +function toHex (value: number, chars: number): string { + const result = value.toString(16) + if (result.length < chars) { + return '0'.repeat(chars - result.length) + result + } + return result +} + +let counter = (Math.random() * (1 << 24)) | 0 +const random = toHex((Math.random() * (1 << 24)) | 0, 6) + toHex((Math.random() * (1 << 16)) | 0, 4) + +function timestamp (): string { + const time = (Date.now() / 1000) | 0 + return toHex(time, 8) +} + +function count (): string { + const val = counter++ & 0xffffff + return toHex(val, 6) +} + +export function generateId (): Ref { + return (timestamp() + random + count()) as Ref +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000000..aeb0517b13 --- /dev/null +++ b/packages/core/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/packages/platform-rig/package.json b/packages/platform-rig/package.json index b2d69907c4..ce6b508565 100644 --- a/packages/platform-rig/package.json +++ b/packages/platform-rig/package.json @@ -6,7 +6,8 @@ }, "devDependencies": { "@rushstack/heft-jest-plugin":"~0.1.15", - "@rushstack/heft":"^0.35.0" + "@rushstack/heft":"^0.35.0", + "eslint-config-standard-with-typescript":"^20.0.0" }, "dependencies": { "@microsoft/api-extractor": "~7.18.4" diff --git a/packages/platform-rig/profiles/default/config/eslint.config.json b/packages/platform-rig/profiles/default/config/eslint.config.json new file mode 100644 index 0000000000..c93dcfcbb0 --- /dev/null +++ b/packages/platform-rig/profiles/default/config/eslint.config.json @@ -0,0 +1,8 @@ +{ + "extends": [ + "standard-with-typescript" + ], + "rules": { + "@typescript-eslint/array-type": "off" + } +} diff --git a/packages/platform-rig/profiles/default/tsconfig.json b/packages/platform-rig/profiles/default/tsconfig.json index 47f6a57536..843003afa4 100644 --- a/packages/platform-rig/profiles/default/tsconfig.json +++ b/packages/platform-rig/profiles/default/tsconfig.json @@ -6,11 +6,6 @@ "strict": true, "sourceMap": true, "resolveJsonModule": true, - "types": ["heft-jest"], - "lib": [ - "esnext", - "esnext.intl", - "dom" - ] + "types": ["heft-jest"] } } \ No newline at end of file diff --git a/packages/platform/.eslintrc.js b/packages/platform/.eslintrc.js index 6497cc0e22..89f8151bd4 100644 --- a/packages/platform/.eslintrc.js +++ b/packages/platform/.eslintrc.js @@ -1,5 +1,5 @@ module.exports = { - extends: 'standard-with-typescript', + extends: ['./node_modules/@anticrm/platform-rig/profiles/default/config/eslint.config.json'], parserOptions: { project: './tsconfig.json' } diff --git a/packages/platform/package.json b/packages/platform/package.json index 8a8839cdce..bd3270fa2b 100644 --- a/packages/platform/package.json +++ b/packages/platform/package.json @@ -5,7 +5,7 @@ "author": "Anticrm Platform Contributors", "license": "EPL-2.0", "scripts": { - "build": "heft build --clean", + "build": "heft build", "build:docs": "api-extractor run --local", "test": "jest", "lint": "ts-standard src", @@ -19,7 +19,6 @@ "eslint-plugin-import":"2", "eslint-plugin-node":"11", "@typescript-eslint/eslint-plugin":"4", - "eslint-config-standard-with-typescript":"^20.0.0", "typescript":"^4.3.5", "eslint":"^7.32.0" }, diff --git a/packages/platform/src/resource.ts b/packages/platform/src/resource.ts index d55e427984..ecc009cb7c 100644 --- a/packages/platform/src/resource.ts +++ b/packages/platform/src/resource.ts @@ -67,7 +67,6 @@ function getLocation (plugin: Plugin): PluginLoader { const loading = new Map>() async function loadPlugin (id: Plugin): Promise { - console.log('loading plugin ', id) let pluginLoader = loading.get(id) if (pluginLoader === undefined) { const status = new Status(Severity.INFO, platform.status.LoadingPlugin, { diff --git a/packages/platform/tsconfig.json b/packages/platform/tsconfig.json index b629b14fd0..dd705c98a5 100644 --- a/packages/platform/tsconfig.json +++ b/packages/platform/tsconfig.json @@ -4,5 +4,6 @@ "compilerOptions": { "rootDir": "./src", "outDir": "./lib", + "lib": ["esnext", "esnext.intl"] } } \ No newline at end of file diff --git a/rush.json b/rush.json index 00d6f63c7f..c4a9cb9a30 100644 --- a/rush.json +++ b/rush.json @@ -446,14 +446,19 @@ // // "versionPolicyName": "" // }, // + { + "packageName": "@anticrm/platform-rig", + "projectFolder": "packages/platform-rig", + "shouldPublish": true + }, { "packageName": "@anticrm/platform", "projectFolder": "packages/platform", "shouldPublish": true }, { - "packageName": "@anticrm/platform-rig", - "projectFolder": "packages/platform-rig", + "packageName": "@anticrm/core", + "projectFolder": "packages/core", "shouldPublish": true } //