mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-09 19:27:44 +02:00
Merge pull request #4 from hcengineering/sync-foundations
Sync with foundations and update versions
This commit is contained in:
@@ -19,8 +19,8 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Verify Change Logs
|
||||
run: node common/scripts/install-run-rush.js change --verify
|
||||
# - name: Verify Change Logs
|
||||
# run: node common/scripts/install-run-rush.js change --verify
|
||||
- name: Rush Install
|
||||
run: node common/scripts/install-run-rush.js install
|
||||
- name: Rush validate
|
||||
|
||||
Generated
+424
-245
File diff suppressed because it is too large
Load Diff
Generated
+3
-3
@@ -21,14 +21,14 @@
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"esbuild": "^0.25.9",
|
||||
"esbuild-plugin-copy": "^2.1.1",
|
||||
"esbuild": "^0.25.10",
|
||||
"esbuild-plugin-copy": "~2.1.1",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"bin": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/scripts",
|
||||
"version": "0.7.14",
|
||||
"version": "0.7.15",
|
||||
"scripts": {
|
||||
"format": "echo \"No format specified\"",
|
||||
"coverage:merge": "node merge-coverage.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/server-client",
|
||||
"version": "0.7.17",
|
||||
"version": "0.7.18",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
@@ -26,24 +26,24 @@
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3",
|
||||
"@types/uuid": "^8.3.1",
|
||||
"@types/ws": "^8.5.11",
|
||||
"@types/ws": "^8.5.12",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/node": "^22.18.1",
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/platform": "^0.7.19",
|
||||
"@hcengineering/core": "^0.7.23",
|
||||
"@hcengineering/client-resources": "^0.7.18",
|
||||
"@hcengineering/client": "^0.7.18",
|
||||
"@hcengineering/account-client": "^0.7.20",
|
||||
"@hcengineering/server-core": "workspace:^0.7.18",
|
||||
"@hcengineering/server-token": "^0.7.17",
|
||||
"@hcengineering/platform": "^0.7.20",
|
||||
"@hcengineering/core": "^0.7.26",
|
||||
"@hcengineering/client-resources": "^0.7.19",
|
||||
"@hcengineering/client": "^0.7.19",
|
||||
"@hcengineering/account-client": "^0.7.25",
|
||||
"@hcengineering/server-core": "workspace:^0.7.19",
|
||||
"@hcengineering/server-token": "^0.7.18",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/huly.server",
|
||||
|
||||
@@ -46,6 +46,8 @@ export class BlobClient {
|
||||
): Promise<void> {
|
||||
let written = 0
|
||||
const chunkSize = 50 * 1024 * 1024
|
||||
let emptyChunkRetries = 0
|
||||
const maxEmptyChunkRetries = 3
|
||||
|
||||
// Use ranges to iterave through file with retry if required.
|
||||
while (written < size) {
|
||||
@@ -65,6 +67,24 @@ export class BlobClient {
|
||||
})
|
||||
const chunk = Buffer.concat(chunks)
|
||||
|
||||
// Check for empty chunk to prevent infinite loop
|
||||
if (chunk.length === 0) {
|
||||
emptyChunkRetries++
|
||||
if (emptyChunkRetries >= maxEmptyChunkRetries) {
|
||||
ctx.error('Empty chunk received multiple times, aborting', { name, written, size, emptyChunkRetries })
|
||||
await new Promise<void>((resolve) => {
|
||||
writable.end(resolve)
|
||||
})
|
||||
throw new Error(
|
||||
`Empty chunk received ${emptyChunkRetries} times for blob ${name} at offset ${written}/${size}`
|
||||
)
|
||||
}
|
||||
ctx.warn('Empty chunk received, retrying', { name, written, size, retry: emptyChunkRetries })
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 100 * emptyChunkRetries))
|
||||
continue
|
||||
}
|
||||
emptyChunkRetries = 0 // Reset on successful non-empty chunk
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
writable.write(chunk, (err) => {
|
||||
if (err != null) {
|
||||
@@ -86,13 +106,13 @@ export class BlobClient {
|
||||
ctx.info('No such key', { name })
|
||||
return
|
||||
}
|
||||
if (i > 4) {
|
||||
if (i >= 4) {
|
||||
await new Promise<void>((resolve) => {
|
||||
writable.end(resolve)
|
||||
})
|
||||
throw err
|
||||
}
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 10))
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 100 * (i + 1)))
|
||||
// retry
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/collaboration",
|
||||
"version": "0.7.17",
|
||||
"version": "0.7.18",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "^0.7.19",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/node": "^22.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
@@ -27,7 +27,7 @@
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
@@ -35,10 +35,10 @@
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "^0.7.23",
|
||||
"@hcengineering/server-core": "workspace:^0.7.18",
|
||||
"@hcengineering/text": "^0.7.18",
|
||||
"@hcengineering/text-ydoc": "^0.7.18",
|
||||
"@hcengineering/core": "^0.7.26",
|
||||
"@hcengineering/server-core": "workspace:^0.7.19",
|
||||
"@hcengineering/text": "^0.7.19",
|
||||
"@hcengineering/text-ydoc": "^0.7.19",
|
||||
"base64-js": "^1.5.1",
|
||||
"yjs": "^13.6.27"
|
||||
},
|
||||
|
||||
+10
-10
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/server-core",
|
||||
"version": "0.7.18",
|
||||
"version": "0.7.19",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "^0.7.19",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/node": "^22.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
@@ -27,7 +27,7 @@
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
@@ -36,13 +36,13 @@
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/analytics": "^0.7.17",
|
||||
"@hcengineering/core": "^0.7.23",
|
||||
"@hcengineering/platform": "^0.7.19",
|
||||
"@hcengineering/query": "^0.7.17",
|
||||
"@hcengineering/rpc": "^0.7.17",
|
||||
"@hcengineering/server-token": "^0.7.17",
|
||||
"@hcengineering/storage": "^0.7.17",
|
||||
"@hcengineering/analytics": "^0.7.19",
|
||||
"@hcengineering/core": "^0.7.26",
|
||||
"@hcengineering/platform": "^0.7.20",
|
||||
"@hcengineering/query": "^0.7.18",
|
||||
"@hcengineering/rpc": "^0.7.18",
|
||||
"@hcengineering/server-token": "^0.7.18",
|
||||
"@hcengineering/storage": "^0.7.18",
|
||||
"fast-equals": "^5.2.2",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
|
||||
@@ -448,7 +448,7 @@ export function runSharedIntegrationTests (adapterName: string, getContext: () =
|
||||
}
|
||||
)
|
||||
expect(r.length).toEqual(1)
|
||||
expect(r[0].$associations?.[association._id][0]?._id).toEqual(secondTask)
|
||||
expect(r[0].$associations?.[`${association._id}_b`][0]?._id).toEqual(secondTask)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -349,6 +349,7 @@ export interface IndexedDoc {
|
||||
searchShortTitle_fields?: any[]
|
||||
searchIcon_fields?: any[]
|
||||
fulltextSummary?: string
|
||||
baseId?: Ref<Doc>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/datalake",
|
||||
"version": "0.7.17",
|
||||
"version": "0.7.18",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
@@ -26,20 +26,20 @@
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/node": "^22.18.1",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"ts-node": "^10.8.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "^0.7.23",
|
||||
"@hcengineering/platform": "^0.7.19",
|
||||
"@hcengineering/server-core": "workspace:^0.7.18",
|
||||
"@hcengineering/server-token": "^0.7.17"
|
||||
"@hcengineering/core": "^0.7.26",
|
||||
"@hcengineering/platform": "^0.7.20",
|
||||
"@hcengineering/server-core": "workspace:^0.7.19",
|
||||
"@hcengineering/server-token": "^0.7.18"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/huly.server",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/elastic",
|
||||
"version": "0.7.17",
|
||||
"version": "0.7.18",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
@@ -44,10 +44,10 @@
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint": "^8.54.0",
|
||||
"ts-node": "^10.8.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
@@ -55,11 +55,11 @@
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "^0.7.23",
|
||||
"@hcengineering/platform": "^0.7.19",
|
||||
"@hcengineering/server-core": "workspace:^0.7.18",
|
||||
"@hcengineering/core": "^0.7.26",
|
||||
"@hcengineering/platform": "^0.7.20",
|
||||
"@hcengineering/server-core": "workspace:^0.7.19",
|
||||
"@elastic/elasticsearch": "^7.17.14",
|
||||
"@hcengineering/analytics": "^0.7.17"
|
||||
"@hcengineering/analytics": "^0.7.19"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/huly.server",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/hulylake",
|
||||
"version": "0.7.17",
|
||||
"version": "0.7.18",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
@@ -26,21 +26,21 @@
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/node": "^22.18.1",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"ts-node": "^10.8.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "^0.7.23",
|
||||
"@hcengineering/platform": "^0.7.19",
|
||||
"@hcengineering/server-core": "workspace:^0.7.18",
|
||||
"@hcengineering/server-token": "^0.7.17",
|
||||
"@hcengineering/hulylake-client": "^0.7.17"
|
||||
"@hcengineering/core": "^0.7.26",
|
||||
"@hcengineering/platform": "^0.7.20",
|
||||
"@hcengineering/server-core": "workspace:^0.7.19",
|
||||
"@hcengineering/server-token": "^0.7.18",
|
||||
"@hcengineering/hulylake-client": "^0.7.18"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/huly.server",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/kafka",
|
||||
"version": "0.7.19",
|
||||
"version": "0.7.20",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
@@ -26,19 +26,19 @@
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/node": "^22.18.1",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "^0.7.23",
|
||||
"@hcengineering/platform": "^0.7.19",
|
||||
"@hcengineering/server-core": "workspace:^0.7.18",
|
||||
"@hcengineering/storage": "^0.7.17",
|
||||
"@hcengineering/core": "^0.7.26",
|
||||
"@hcengineering/platform": "^0.7.20",
|
||||
"@hcengineering/server-core": "workspace:^0.7.19",
|
||||
"@hcengineering/storage": "^0.7.18",
|
||||
"kafkajs": "^2.2.4"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/huly.server",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/middleware",
|
||||
"version": "0.7.22",
|
||||
"version": "0.7.23",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
@@ -26,20 +26,21 @@
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/node": "^22.18.1",
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "^0.7.23",
|
||||
"@hcengineering/platform": "^0.7.19",
|
||||
"@hcengineering/server-core": "workspace:^0.7.18",
|
||||
"@hcengineering/query": "^0.7.17",
|
||||
"@hcengineering/analytics": "^0.7.17",
|
||||
"@hcengineering/core": "^0.7.26",
|
||||
"@hcengineering/contact": "^0.7.0",
|
||||
"@hcengineering/platform": "^0.7.20",
|
||||
"@hcengineering/server-core": "workspace:^0.7.19",
|
||||
"@hcengineering/query": "^0.7.18",
|
||||
"@hcengineering/analytics": "^0.7.19",
|
||||
"fast-equals": "^5.2.2"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/huly.server",
|
||||
|
||||
@@ -63,6 +63,14 @@ export class ApplyTxMiddleware extends BaseMiddleware implements Middleware {
|
||||
}
|
||||
applyResult.serverTime = Date.now() - st
|
||||
} else {
|
||||
ctx.warn('TxApplyIf failed', {
|
||||
scope: applyIf.scope,
|
||||
reason: passed.reason,
|
||||
measureName: applyIf.measureName,
|
||||
matchCount: applyIf.match?.length ?? 0,
|
||||
notMatchCount: applyIf.notMatch?.length ?? 0,
|
||||
txCount: applyIf.txes.length
|
||||
})
|
||||
result.push({
|
||||
success: false
|
||||
})
|
||||
@@ -92,6 +100,7 @@ export class ApplyTxMiddleware extends BaseMiddleware implements Middleware {
|
||||
): Promise<{
|
||||
onEnd: () => void
|
||||
passed: boolean
|
||||
reason?: string
|
||||
}> {
|
||||
if (applyIf.scope == null) {
|
||||
return { passed: true, onEnd: () => {} }
|
||||
@@ -115,11 +124,13 @@ export class ApplyTxMiddleware extends BaseMiddleware implements Middleware {
|
||||
})
|
||||
)
|
||||
let passed = true
|
||||
let reason: string | undefined
|
||||
if (applyIf.match != null) {
|
||||
for (const { _class, query } of applyIf.match) {
|
||||
const res = await this.provideFindAll(ctx, _class, query, { limit: 1 })
|
||||
if (res.length === 0) {
|
||||
passed = false
|
||||
reason = `match query failed: class=${_class}, query=${JSON.stringify(query)}`
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -129,10 +140,11 @@ export class ApplyTxMiddleware extends BaseMiddleware implements Middleware {
|
||||
const res = await this.provideFindAll(ctx, _class, query, { limit: 1 })
|
||||
if (res.length > 0) {
|
||||
passed = false
|
||||
reason = `notMatch query failed: class=${_class}, query=${JSON.stringify(query)} (found ${res.length} matching document(s))`
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return { passed, onEnd }
|
||||
return { passed, onEnd, reason }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type TxMiddlewareResult
|
||||
} from '@hcengineering/server-core'
|
||||
import core, {
|
||||
type Account,
|
||||
AccountRole,
|
||||
type Doc,
|
||||
hasAccountRole,
|
||||
@@ -19,6 +20,7 @@ import core, {
|
||||
type TxUpdateDoc
|
||||
} from '@hcengineering/core'
|
||||
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
|
||||
import contact, { type Person } from '@hcengineering/contact'
|
||||
|
||||
export class GuestPermissionsMiddleware extends BaseMiddleware implements Middleware {
|
||||
static async create (
|
||||
@@ -40,44 +42,44 @@ export class GuestPermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
}
|
||||
|
||||
for (const tx of txes) {
|
||||
this.processTx(ctx, tx)
|
||||
await this.processTx(ctx, tx)
|
||||
}
|
||||
|
||||
return await this.provideTx(ctx, txes)
|
||||
}
|
||||
|
||||
private processTx (ctx: MeasureContext<SessionData>, tx: Tx): void {
|
||||
private async processTx (ctx: MeasureContext<SessionData>, tx: Tx): Promise<void> {
|
||||
const h = this.context.hierarchy
|
||||
if (tx._class === core.class.TxApplyIf) {
|
||||
const applyTx = tx as TxApplyIf
|
||||
for (const t of applyTx.txes) {
|
||||
this.processTx(ctx, t)
|
||||
await this.processTx(ctx, t)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (TxProcessor.isExtendsCUD(tx._class)) {
|
||||
const socialIds = ctx.contextData.account.socialIds
|
||||
const { account } = ctx.contextData
|
||||
const cudTx = tx as TxCUD<Doc>
|
||||
const isSpace = h.isDerived(cudTx.objectClass, core.class.Space)
|
||||
if (isSpace) {
|
||||
if (this.isForbiddenSpaceTx(cudTx as TxCUD<Space>, socialIds)) {
|
||||
if (await this.isForbiddenSpaceTx(ctx, cudTx as TxCUD<Space>, account)) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
} else if (cudTx.space !== core.space.DerivedTx && this.isForbiddenTx(cudTx, socialIds)) {
|
||||
} else if (cudTx.space !== core.space.DerivedTx && (await this.isForbiddenTx(ctx, cudTx, account))) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isForbiddenTx (tx: TxCUD<Doc>, socialIds: PersonId[]): boolean {
|
||||
private async isForbiddenTx (ctx: MeasureContext, tx: TxCUD<Doc>, account: Account): Promise<boolean> {
|
||||
if (tx._class === core.class.TxMixin) return false
|
||||
return !this.hasMixinAccessLevel(tx, socialIds)
|
||||
return !(await this.hasMixinAccessLevel(ctx, tx, account))
|
||||
}
|
||||
|
||||
private isForbiddenSpaceTx (tx: TxCUD<Space>, socialIds: PersonId[]): boolean {
|
||||
private async isForbiddenSpaceTx (ctx: MeasureContext, tx: TxCUD<Space>, account: Account): Promise<boolean> {
|
||||
if (tx._class === core.class.TxRemoveDoc) return true
|
||||
if (tx._class === core.class.TxCreateDoc) {
|
||||
return !this.hasMixinAccessLevel(tx, socialIds)
|
||||
return !(await this.hasMixinAccessLevel(ctx, tx, account))
|
||||
}
|
||||
if (tx._class === core.class.TxUpdateDoc) {
|
||||
const updateTx = tx as TxUpdateDoc<Space>
|
||||
@@ -93,7 +95,7 @@ export class GuestPermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
return false
|
||||
}
|
||||
|
||||
private hasMixinAccessLevel (tx: TxCUD<Doc>, socialIds: PersonId[]): boolean {
|
||||
private async hasMixinAccessLevel (ctx: MeasureContext, tx: TxCUD<Doc>, account: Account): Promise<boolean> {
|
||||
const h = this.context.hierarchy
|
||||
const accessLevelMixin = h.classHierarchyMixin(tx.objectClass, core.mixin.TxAccessLevel)
|
||||
if (accessLevelMixin === undefined) return false
|
||||
@@ -104,9 +106,15 @@ export class GuestPermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
return accessLevelMixin.removeAccessLevel === AccountRole.Guest
|
||||
}
|
||||
if (tx._class === core.class.TxUpdateDoc) {
|
||||
if (accessLevelMixin.isIdentity === true && socialIds.includes(tx.objectId as unknown as PersonId)) {
|
||||
if (accessLevelMixin.isIdentity === true && account.socialIds.includes(tx.objectId as unknown as PersonId)) {
|
||||
return true
|
||||
}
|
||||
if (accessLevelMixin.isIdentity === true && h.isDerived(tx.objectClass, contact.class.Person)) {
|
||||
const person = (await this.findAll(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0] as
|
||||
| Person
|
||||
| undefined
|
||||
return person?.personUuid === account.uuid
|
||||
}
|
||||
return accessLevelMixin.updateAccessLevel === AccountRole.Guest
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -43,3 +43,4 @@ export * from './pluginConfig'
|
||||
export * from './userStatus'
|
||||
export * from './findSecurity'
|
||||
export * from './normalizeTx'
|
||||
export * from './versioning'
|
||||
|
||||
@@ -37,7 +37,7 @@ import core, {
|
||||
} from '@hcengineering/core'
|
||||
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
|
||||
import { type Middleware, type TxMiddlewareResult, type PipelineContext } from '@hcengineering/server-core'
|
||||
|
||||
import contact from '@hcengineering/contact'
|
||||
import { BaseMiddleware } from '@hcengineering/server-core'
|
||||
|
||||
/**
|
||||
@@ -45,6 +45,7 @@ import { BaseMiddleware } from '@hcengineering/server-core'
|
||||
*/
|
||||
export class SpacePermissionsMiddleware extends BaseMiddleware implements Middleware {
|
||||
private whitelistSpaces = new Set<Ref<Space>>()
|
||||
private readonly restrictedSpaces = new Set<Ref<Space>>()
|
||||
private assignmentBySpace: Record<Ref<Space>, RolesAssignment> = {}
|
||||
private permissionsBySpace: Record<Ref<Space>, Record<AccountUuid, Set<Permission>>> = {}
|
||||
private typeBySpace: Record<Ref<Space>, Ref<SpaceType>> = {}
|
||||
@@ -121,6 +122,12 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
return
|
||||
}
|
||||
|
||||
if (space.restricted === true) {
|
||||
this.restrictedSpaces.add(space._id)
|
||||
} else {
|
||||
this.restrictedSpaces.delete(space._id)
|
||||
}
|
||||
|
||||
this.typeBySpace[space._id] = space.type
|
||||
|
||||
const asMixin: RolesAssignment = this.context.hierarchy.as(
|
||||
@@ -157,15 +164,21 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
*
|
||||
* Checks if the required permission is present in the space for the given context
|
||||
*/
|
||||
private checkPermission (ctx: MeasureContext<SessionData>, space: Ref<TypedSpace>, tx: TxCUD<Doc>): boolean {
|
||||
private checkPermission (
|
||||
ctx: MeasureContext<SessionData>,
|
||||
space: Ref<TypedSpace>,
|
||||
tx: TxCUD<Doc>,
|
||||
isSpace: boolean
|
||||
): boolean {
|
||||
const account = ctx.contextData.account
|
||||
if (account.primarySocialId === core.account.System) return true
|
||||
const permissions = this.permissionsBySpace[space]?.[account.uuid] ?? []
|
||||
let withoutMatch: Permission | undefined
|
||||
for (const permission of permissions) {
|
||||
if (permission.txClass === undefined || permission.txClass !== tx._class) continue
|
||||
if (!isTxClassMatched(tx, permission)) continue
|
||||
if (
|
||||
permission.objectClass !== undefined &&
|
||||
!this.context.hierarchy.isDerived(tx.objectClass, permission.objectClass)
|
||||
!this.context.hierarchy.isDerived(getTxObjectClass(tx), permission.objectClass)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
@@ -185,6 +198,36 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
return withoutMatch.forbid !== undefined ? !withoutMatch.forbid : true
|
||||
}
|
||||
|
||||
if (isSpace || !this.restrictedSpaces.has(space)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const attachedDocAncestors = this.context.hierarchy.getAncestors(core.class.AttachedDoc)
|
||||
const ancestors = this.context.hierarchy.getAncestors(getTxObjectClass(tx))
|
||||
const targetAncestors = ancestors.filter((a) => !attachedDocAncestors.includes(a))
|
||||
|
||||
const allPermissions = this.context.modelDb.findAllSync(core.class.Permission, {
|
||||
objectClass: { $in: targetAncestors }
|
||||
})
|
||||
for (const permission of allPermissions) {
|
||||
if (!isTxClassMatched(tx, permission)) continue
|
||||
if (
|
||||
permission.objectClass !== undefined &&
|
||||
!this.context.hierarchy.isDerived(getTxObjectClass(tx), permission.objectClass)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (permission.txMatch === undefined) {
|
||||
return false
|
||||
} else {
|
||||
const checkMatch = matchQuery([tx], permission.txMatch, tx._class, this.context.hierarchy, true)
|
||||
if (checkMatch.length === 0) {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -273,6 +316,7 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
delete this.typeBySpace[tx.objectId]
|
||||
|
||||
this.whitelistSpaces.delete(tx.objectId)
|
||||
this.restrictedSpaces.delete(tx.objectId)
|
||||
}
|
||||
|
||||
private isSpaceTxCUD (tx: TxCUD<Doc>): tx is TxCUD<Space> {
|
||||
@@ -337,9 +381,8 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
if (this.isSpaceTxCUD(tx)) {
|
||||
if (tx._class === core.class.TxCreateDoc) {
|
||||
this.handleCreate(tx)
|
||||
// } else if (tx._class === core.class.TxUpdateDoc) {
|
||||
// Roles assignment in spaces are managed through the space type mixin
|
||||
// so nothing to handle here
|
||||
} else if (tx._class === core.class.TxUpdateDoc) {
|
||||
this.handleSpaceUpdate(tx)
|
||||
} else if (tx._class === core.class.TxMixin) {
|
||||
this.handleMixin(tx)
|
||||
} else if (tx._class === core.class.TxRemoveDoc) {
|
||||
@@ -350,6 +393,21 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
this.handlePermissionsUpdatesFromRoleTx(ctx, tx)
|
||||
}
|
||||
|
||||
private handleSpaceUpdate (tx: TxCUD<Space>): void {
|
||||
if (!this.isTypedSpaceClass(tx.objectClass)) {
|
||||
return
|
||||
}
|
||||
|
||||
const updateTx = tx as TxUpdateDoc<TypedSpace>
|
||||
if (updateTx.operations.restricted !== undefined) {
|
||||
if (updateTx.operations.restricted) {
|
||||
this.restrictedSpaces.add(tx.objectId)
|
||||
} else {
|
||||
this.restrictedSpaces.delete(tx.objectId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private processPermissionsUpdatesFromTx (ctx: MeasureContext, tx: Tx): void {
|
||||
if (!TxProcessor.isExtendsCUD(tx._class)) {
|
||||
return
|
||||
@@ -362,8 +420,8 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
async tx (ctx: MeasureContext<SessionData>, txes: Tx[]): Promise<TxMiddlewareResult> {
|
||||
await this.init(ctx)
|
||||
for (const tx of txes) {
|
||||
this.processPermissionsUpdatesFromTx(ctx, tx)
|
||||
this.checkPermissions(ctx, tx)
|
||||
this.processPermissionsUpdatesFromTx(ctx, tx)
|
||||
}
|
||||
const res = await this.provideTx(ctx, txes)
|
||||
for (const txd of ctx.contextData.broadcast.txes) {
|
||||
@@ -388,18 +446,50 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
|
||||
this.checkSpacePermissions(ctx, cudTx, cudTx.objectSpace)
|
||||
if (isSpace) {
|
||||
this.checkSpacePermissions(ctx, cudTx, cudTx.objectId as Ref<Space>)
|
||||
this.checkSpaceTypePermissions(ctx, cudTx as TxCUD<Space>)
|
||||
this.checkSpacePermissions(ctx, cudTx, cudTx.objectId as Ref<Space>, true)
|
||||
}
|
||||
}
|
||||
|
||||
private checkSpacePermissions (ctx: MeasureContext, cudTx: TxCUD<Doc>, targetSpaceId: Ref<Space>): void {
|
||||
private checkSpaceTypePermissions (ctx: MeasureContext, cudTx: TxCUD<Space>): void {
|
||||
const account = ctx.contextData.account
|
||||
const h = this.context.hierarchy
|
||||
if (account.primarySocialId === core.account.System) return
|
||||
|
||||
if (h.isDerived(cudTx.objectClass, contact.class.PersonSpace)) {
|
||||
this.throwForbidden()
|
||||
}
|
||||
}
|
||||
|
||||
private checkSpacePermissions (
|
||||
ctx: MeasureContext,
|
||||
cudTx: TxCUD<Doc>,
|
||||
targetSpaceId: Ref<Space>,
|
||||
isSpace: boolean = false
|
||||
): void {
|
||||
if (this.whitelistSpaces.has(targetSpaceId)) {
|
||||
return
|
||||
}
|
||||
// NOTE: move this checking logic later to be defined in some server plugins?
|
||||
// so they can contribute checks into the middleware for their custom permissions?
|
||||
if (!this.checkPermission(ctx, targetSpaceId as Ref<TypedSpace>, cudTx)) {
|
||||
if (!this.checkPermission(ctx, targetSpaceId as Ref<TypedSpace>, cudTx, isSpace)) {
|
||||
this.throwForbidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isMixinUpdateTx (tx: Tx): boolean {
|
||||
return tx._class === core.class.TxMixin && Object.keys((tx as TxMixin<Doc, Doc>).attributes).length > 0
|
||||
}
|
||||
|
||||
function isTxClassMatched (tx: Tx, permission: Permission): boolean {
|
||||
if (permission.txClass === tx._class) return true
|
||||
if (permission.txMatch === undefined && isMixinUpdateTx(tx)) {
|
||||
return permission.txClass === core.class.TxUpdateDoc
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function getTxObjectClass (tx: TxCUD<Doc>): Ref<Class<Doc>> {
|
||||
return tx._class === core.class.TxMixin ? (tx as TxMixin<Doc, Doc>).mixin : tx.objectClass
|
||||
}
|
||||
|
||||
@@ -484,10 +484,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
||||
const space = this.spacesMap.get(tx.objectSpace)
|
||||
if (space === undefined) return undefined
|
||||
|
||||
// For all other spaces broadcast to space members + guests that are collaborators for objects with collab security enabled
|
||||
let collabTargets: AccountUuid[] = []
|
||||
const collabSec = getClassCollaborators(this.context.modelDb, this.context.hierarchy, cud.objectClass)
|
||||
if (collabSec?.provideSecurity === true) {
|
||||
const getCollabTargets = async (_id: Ref<Doc>): Promise<AccountUuid[]> => {
|
||||
const guests = new Set<AccountUuid>()
|
||||
for (const val of ctx.contextData.socialStringsToUsers.values()) {
|
||||
if ([AccountRole.Guest, AccountRole.ReadOnlyGuest].includes(val.role)) {
|
||||
@@ -495,11 +492,30 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
||||
}
|
||||
}
|
||||
const collaboratorObjs = (await this.next?.findAll(ctx, core.class.Collaborator, {
|
||||
attachedTo: cud.objectId
|
||||
attachedTo: _id
|
||||
})) as Collaborator[]
|
||||
|
||||
collabTargets = collaboratorObjs.map((it) => it.collaborator).filter((it) => guests.has(it))
|
||||
return collaboratorObjs.map((it) => it.collaborator).filter((it) => guests.has(it))
|
||||
}
|
||||
|
||||
// For all other spaces broadcast to space members
|
||||
// + guests that are collaborators for objects with collab security enabled
|
||||
// + guests that are collaborators for attached objects with collab security enabled
|
||||
let collabTargets: AccountUuid[] = []
|
||||
const collabSec = getClassCollaborators(this.context.modelDb, this.context.hierarchy, cud.objectClass)
|
||||
if (collabSec?.provideSecurity === true) {
|
||||
collabTargets = await getCollabTargets(cud.objectId)
|
||||
} else if (cud.attachedTo != null && cud.attachedToClass != null) {
|
||||
const attachedCollabSec = getClassCollaborators(
|
||||
this.context.modelDb,
|
||||
this.context.hierarchy,
|
||||
cud.attachedToClass
|
||||
)
|
||||
if (attachedCollabSec?.provideSecurity === true) {
|
||||
collabTargets = await getCollabTargets(cud.attachedTo)
|
||||
}
|
||||
}
|
||||
|
||||
const spaceTargets = space.members.length === 0 ? [] : this.getTargets(space?.members)
|
||||
const target = [...collabTargets, ...spaceTargets]
|
||||
|
||||
@@ -672,7 +688,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
||||
findResult.lookupMap
|
||||
)
|
||||
}
|
||||
if (!isOwner(account, ctx) && account.role !== AccountRole.DocGuest) {
|
||||
if (account.role !== AccountRole.DocGuest) {
|
||||
if (options?.lookup !== undefined) {
|
||||
for (const object of findResult) {
|
||||
if (object.$lookup !== undefined) {
|
||||
@@ -721,20 +737,53 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
||||
if (Object.keys(lookup).length === 0) return
|
||||
const account = ctx.contextData.account
|
||||
if (isSystem(account, ctx)) return
|
||||
const owner = isOwner(account, ctx)
|
||||
const h = this.context.hierarchy
|
||||
const allowedSpaces = new Set(this.getAllAllowedSpaces(account, true, showArchived))
|
||||
for (const key in lookup) {
|
||||
const val = lookup[key]
|
||||
if (Array.isArray(val)) {
|
||||
const arr: AttachedDoc[] = []
|
||||
for (const value of val) {
|
||||
if (allowedSpaces.has(value.space)) {
|
||||
const isSpace = '_class' in value && h.isDerived(value._class, core.class.Space)
|
||||
const availableForOwner = owner && isSpace
|
||||
const availableSpace = isSpace && allowedSpaces.has(value._id)
|
||||
const availableDoc = !isSpace && allowedSpaces.has(value.space)
|
||||
if (availableForOwner || availableSpace || availableDoc) {
|
||||
arr.push(value)
|
||||
}
|
||||
}
|
||||
lookup[key] = arr as any
|
||||
} else if (val !== undefined) {
|
||||
if (!allowedSpaces.has(val.space)) {
|
||||
lookup[key] = undefined
|
||||
const isSpace = '_class' in val && h.isDerived(val._class, core.class.Space)
|
||||
const availableForOwner = owner && isSpace
|
||||
const availableSpace = isSpace && allowedSpaces.has(val._id as Ref<Space>)
|
||||
const availableDoc = !isSpace && allowedSpaces.has(val.space)
|
||||
if (!availableForOwner && !availableSpace && !availableDoc) {
|
||||
// allow attached lookups for guests when collaborator security is enabled
|
||||
// do not check if collaborator of the doc because it's being checked on the storage (DB) level
|
||||
// as otherwise there will be no doc here at all
|
||||
if (key === 'attachedTo' && ctx.contextData.modelDb?.hierarchy != null) {
|
||||
const attachedVal = val as AttachedDoc
|
||||
if (attachedVal.attachedToClass == null) {
|
||||
lookup[key] = undefined
|
||||
continue
|
||||
}
|
||||
|
||||
const collabSec = getClassCollaborators(
|
||||
ctx.contextData.modelDb,
|
||||
ctx.contextData.modelDb.hierarchy,
|
||||
attachedVal.attachedToClass
|
||||
)
|
||||
const collabSecEnabled =
|
||||
collabSec?.provideSecurity === true &&
|
||||
[AccountRole.Guest, AccountRole.ReadOnlyGuest].includes(account.role)
|
||||
if (!collabSecEnabled) {
|
||||
lookup[key] = undefined
|
||||
}
|
||||
} else {
|
||||
lookup[key] = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
//
|
||||
// Copyright © 2025 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 core, {
|
||||
type Class,
|
||||
clone,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
type FindResult,
|
||||
type MeasureContext,
|
||||
type Ref,
|
||||
type SessionData,
|
||||
SortingOrder,
|
||||
type Tx,
|
||||
type TxApplyIf,
|
||||
type TxCreateDoc,
|
||||
TxFactory,
|
||||
TxProcessor,
|
||||
type VersionableDoc
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
BaseMiddleware,
|
||||
type ServerFindOptions,
|
||||
type Middleware,
|
||||
type PipelineContext,
|
||||
type TxMiddlewareResult
|
||||
} from '@hcengineering/server-core'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class VersioningMiddleware extends BaseMiddleware implements Middleware {
|
||||
private constructor (context: PipelineContext, next?: Middleware) {
|
||||
super(context, next)
|
||||
}
|
||||
|
||||
static async create (
|
||||
ctx: MeasureContext,
|
||||
context: PipelineContext,
|
||||
next: Middleware | undefined
|
||||
): Promise<VersioningMiddleware> {
|
||||
return new VersioningMiddleware(context, next)
|
||||
}
|
||||
|
||||
override async findAll<T extends Doc>(
|
||||
ctx: MeasureContext<SessionData>,
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: ServerFindOptions<T>
|
||||
): Promise<FindResult<T>> {
|
||||
if (
|
||||
this.isVerionableClass(_class) &&
|
||||
query.isLatest === undefined &&
|
||||
query._id === undefined &&
|
||||
query.baseId === undefined
|
||||
) {
|
||||
const newQuery = clone(query)
|
||||
newQuery.isLatest = true
|
||||
|
||||
const findResult = await this.provideFindAll(ctx, _class, newQuery, options)
|
||||
|
||||
return findResult
|
||||
} else {
|
||||
return await this.provideFindAll(ctx, _class, query, options)
|
||||
}
|
||||
}
|
||||
|
||||
async tx (ctx: MeasureContext<SessionData>, txes: Tx[]): Promise<TxMiddlewareResult> {
|
||||
let nestedTxes: Tx[] = []
|
||||
for (const tx of txes) {
|
||||
if (tx._class === core.class.TxCreateDoc) {
|
||||
const childTxes = await this.setVersionData(ctx, tx as TxCreateDoc<VersionableDoc>)
|
||||
if (childTxes !== undefined && childTxes.length > 0) {
|
||||
nestedTxes = nestedTxes.concat(childTxes)
|
||||
}
|
||||
}
|
||||
if (tx._class === core.class.TxApplyIf) {
|
||||
for (const _tx of (tx as TxApplyIf).txes) {
|
||||
if (_tx._class === core.class.TxCreateDoc) {
|
||||
const childTxes = await this.setVersionData(ctx, _tx as TxCreateDoc<VersionableDoc>)
|
||||
if (childTxes !== undefined && childTxes.length > 0) {
|
||||
nestedTxes = nestedTxes.concat(childTxes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const res = await this.provideTx(ctx, txes)
|
||||
if (nestedTxes.length > 0) {
|
||||
await this.provideTx(ctx, nestedTxes)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
private async setVersionData (
|
||||
ctx: MeasureContext<SessionData>,
|
||||
tx: TxCreateDoc<VersionableDoc>
|
||||
): Promise<Tx[] | undefined> {
|
||||
const isVersionedClass = this.isVerionableClass(tx.objectClass)
|
||||
if (!isVersionedClass) return []
|
||||
const doc = TxProcessor.createDoc2Doc(tx)
|
||||
const isNew = doc.baseId === doc._id || doc.baseId === undefined
|
||||
tx.attributes.isLatest = true
|
||||
if (isNew) {
|
||||
tx.attributes.version = 1
|
||||
tx.attributes.baseId = tx.objectId
|
||||
tx.attributes.docCreatedBy = tx.createdBy ?? tx.modifiedBy
|
||||
} else {
|
||||
const base = await this.provideFindAll(
|
||||
ctx,
|
||||
tx.objectClass,
|
||||
{ baseId: doc.baseId },
|
||||
{ sort: { version: SortingOrder.Descending } }
|
||||
)
|
||||
const latest = base.find((p) => p.isLatest === true) ?? base[0]
|
||||
if (latest === undefined) throw new Error('No base object found for the new version')
|
||||
tx.attributes.version = (latest.version ?? 1) + 1
|
||||
tx.attributes.docCreatedBy = latest.docCreatedBy
|
||||
const txes: Tx[] = []
|
||||
const factory = new TxFactory(core.account.System, true)
|
||||
for (const prev of base) {
|
||||
if (prev.isLatest === true) {
|
||||
txes.push(
|
||||
factory.createTxUpdateDoc(prev._class, prev.space, prev._id, {
|
||||
isLatest: false,
|
||||
readonly: true
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
return txes
|
||||
}
|
||||
}
|
||||
|
||||
private isVerionableClass (_class: Ref<Class<Doc>>): boolean {
|
||||
try {
|
||||
return this.context.hierarchy.classHierarchyMixin(_class, core.mixin.VersionableClass) !== undefined
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/minio",
|
||||
"version": "0.7.17",
|
||||
"version": "0.7.18",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
@@ -26,18 +26,18 @@
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/node": "^22.18.1",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "^0.7.23",
|
||||
"@hcengineering/platform": "^0.7.19",
|
||||
"@hcengineering/server-core": "workspace:^0.7.18",
|
||||
"@hcengineering/core": "^0.7.26",
|
||||
"@hcengineering/platform": "^0.7.20",
|
||||
"@hcengineering/server-core": "workspace:^0.7.19",
|
||||
"minio": "^8.0.5"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/huly.server",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/mongo",
|
||||
"version": "0.7.17",
|
||||
"version": "0.7.18",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
@@ -26,18 +26,18 @@
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/node": "^22.18.1",
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "^0.7.23",
|
||||
"@hcengineering/platform": "^0.7.19",
|
||||
"@hcengineering/server-core": "workspace:^0.7.18",
|
||||
"@hcengineering/core": "^0.7.26",
|
||||
"@hcengineering/platform": "^0.7.20",
|
||||
"@hcengineering/server-core": "workspace:^0.7.19",
|
||||
"mongodb": "^6.16.0",
|
||||
"bson": "^6.10.3"
|
||||
},
|
||||
|
||||
@@ -323,7 +323,78 @@ describe('mongo operations', () => {
|
||||
}
|
||||
)
|
||||
expect(r.length).toEqual(1)
|
||||
expect((r[0].$associations?.[association._id][0] as unknown as Task)?._id).toEqual(secondTask)
|
||||
expect((r[0].$associations?.[association._id + '_b'][0] as unknown as Task)?._id).toEqual(secondTask)
|
||||
})
|
||||
|
||||
it('check deep associations', async () => {
|
||||
const association = await operations.findOne(core.class.Association, {})
|
||||
if (association == null) {
|
||||
throw new Error('Association not found')
|
||||
}
|
||||
|
||||
const zeroTask = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
|
||||
name: 'my-task',
|
||||
description: 'Descr',
|
||||
rate: 20
|
||||
})
|
||||
|
||||
const firstTask = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
|
||||
name: 'my-task',
|
||||
description: 'Descr',
|
||||
rate: 20
|
||||
})
|
||||
|
||||
const secondTask = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
|
||||
name: 'my-task2',
|
||||
description: 'Descr',
|
||||
rate: 20
|
||||
})
|
||||
|
||||
const secondATask = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
|
||||
name: 'my-task22',
|
||||
description: 'Descr',
|
||||
rate: 20
|
||||
})
|
||||
|
||||
const thirdTask = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
|
||||
name: 'my-task3',
|
||||
description: 'Descr',
|
||||
rate: 20
|
||||
})
|
||||
|
||||
await operations.createDoc(core.class.Relation, '' as Ref<Space>, {
|
||||
docA: firstTask,
|
||||
docB: secondTask,
|
||||
association: association._id
|
||||
})
|
||||
|
||||
await operations.createDoc(core.class.Relation, '' as Ref<Space>, {
|
||||
docA: firstTask,
|
||||
docB: secondATask,
|
||||
association: association._id
|
||||
})
|
||||
|
||||
await operations.createDoc(core.class.Relation, '' as Ref<Space>, {
|
||||
docA: secondTask,
|
||||
docB: thirdTask,
|
||||
association: association._id
|
||||
})
|
||||
|
||||
const r = await client.findAll(
|
||||
taskPlugin.class.Task,
|
||||
{ _id: { $in: [zeroTask, firstTask] } },
|
||||
{
|
||||
associations: [[association._id, 1, [[association._id, 1]]]]
|
||||
}
|
||||
)
|
||||
expect(r.length).toEqual(2)
|
||||
expect(r[1].$associations?.[`${association._id}_b`]).toHaveLength(2)
|
||||
expect((r[1].$associations?.[`${association._id}_b`][0] as unknown as Task)?._id).toEqual(secondTask)
|
||||
expect(r[1].$associations?.[`${association._id}_b`][1]?.$associations?.[`${association._id}_b`]).toHaveLength(0)
|
||||
expect(
|
||||
(r[1].$associations?.[`${association._id}_b`][0]?.$associations?.[`${association._id}_b`][0] as unknown as Task)
|
||||
?._id
|
||||
).toEqual(thirdTask)
|
||||
})
|
||||
|
||||
// Run shared integration tests
|
||||
|
||||
@@ -467,19 +467,21 @@ abstract class MongoAdapterBase implements DbAdapter {
|
||||
return result
|
||||
}
|
||||
|
||||
private getAssociations (associations: AssociationQuery[]): LookupStep[] {
|
||||
private getAssociations (associations: AssociationQuery[], parentId: string = ''): LookupStep[] {
|
||||
const res: LookupStep[] = []
|
||||
for (const association of associations) {
|
||||
const assoc = this.modelDb.findObject(association[0])
|
||||
const _id = association[0]
|
||||
const assoc = this.modelDb.findObject(_id)
|
||||
if (assoc === undefined) continue
|
||||
const isReverse = association[1] === -1
|
||||
const _class = !isReverse ? assoc.classB : assoc.classA
|
||||
const fullId = _id + (isReverse ? '_a' : '_b')
|
||||
const targetDomain = this.hierarchy.getDomain(_class)
|
||||
if (targetDomain === DOMAIN_MODEL) continue
|
||||
const as = association[0] + '_hidden_association'
|
||||
const as = parentId + fullId + '_hidden_association'
|
||||
res.push({
|
||||
from: DOMAIN_RELATION,
|
||||
localField: '_id',
|
||||
localField: `${parentId !== '' ? parentId + '.' : ''}_id`,
|
||||
foreignField: isReverse ? 'docB' : 'docA',
|
||||
as
|
||||
})
|
||||
@@ -487,8 +489,11 @@ abstract class MongoAdapterBase implements DbAdapter {
|
||||
from: targetDomain,
|
||||
localField: as + '.' + (isReverse ? 'docA' : 'docB'),
|
||||
foreignField: '_id',
|
||||
as: association[0] + '_association'
|
||||
as: parentId + fullId + '_association'
|
||||
})
|
||||
if (association[2] !== undefined) {
|
||||
res.push(...this.getAssociations(association[2], parentId + fullId + '_association'))
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -518,19 +523,36 @@ abstract class MongoAdapterBase implements DbAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
private fillAssociationsValue (associations: AssociationQuery[], object: any): Record<string, Doc[]> {
|
||||
private fillAssociationsValue (
|
||||
source: any,
|
||||
associations: AssociationQuery[],
|
||||
parentKey: string,
|
||||
targetId: Ref<Doc>
|
||||
): Record<string, Doc[]> {
|
||||
const res: Record<string, Doc[]> = {}
|
||||
for (const association of associations) {
|
||||
const assocKey = association[0] + '_hidden_association'
|
||||
const data = object[assocKey]
|
||||
const _id = association[0]
|
||||
const isReverse = association[1] === -1
|
||||
// const key = _id + (isReverse ? '.a' : '.b')
|
||||
const fullId = _id + (isReverse ? '_a' : '_b')
|
||||
const assocKey = parentKey + fullId + '_hidden_association'
|
||||
const data = source[assocKey]
|
||||
if (data !== undefined && Array.isArray(data)) {
|
||||
const filtered = new Set(
|
||||
data.filter((it) => it.association === association[0]).map((it) => (association[1] === 1 ? it.docB : it.docA))
|
||||
data
|
||||
.filter((it) => it.association === _id && (isReverse ? it.docB : it.docA) === targetId)
|
||||
.map((it) => (!isReverse ? it.docB : it.docA))
|
||||
)
|
||||
const fullKey = association[0] + '_association'
|
||||
const arr = object[fullKey]
|
||||
const fullKey = parentKey + fullId + '_association'
|
||||
const arr = source[fullKey] as WithLookup<Doc>[]
|
||||
if (arr !== undefined && Array.isArray(arr)) {
|
||||
res[association[0]] = arr.filter((it) => filtered.has(it._id))
|
||||
const objects = arr.filter((it) => filtered.has(it._id))
|
||||
if (association[2] !== undefined) {
|
||||
for (const obj of objects) {
|
||||
this.fillAssociations(obj, association[2], fullKey, source)
|
||||
}
|
||||
}
|
||||
res[fullId] = objects
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -718,10 +740,7 @@ abstract class MongoAdapterBase implements DbAdapter {
|
||||
}
|
||||
}
|
||||
if (options.associations !== undefined && options.associations.length > 0) {
|
||||
row.$associations = this.fillAssociationsValue(options.associations, row)
|
||||
for (const [, v] of Object.entries(row.$associations)) {
|
||||
this.stripHash(v)
|
||||
}
|
||||
this.fillAssociations(row, options.associations)
|
||||
}
|
||||
this.clearExtraLookups(row)
|
||||
}
|
||||
@@ -756,6 +775,19 @@ abstract class MongoAdapterBase implements DbAdapter {
|
||||
return toFindResult(this.stripHash(result) as T[], total)
|
||||
}
|
||||
|
||||
private fillAssociations<T extends Doc>(
|
||||
obj: WithLookup<T>,
|
||||
associations: AssociationQuery[],
|
||||
parentId: string = '',
|
||||
source?: Doc
|
||||
): WithLookup<T> {
|
||||
obj.$associations = this.fillAssociationsValue(source ?? obj, associations, parentId, obj._id)
|
||||
for (const [, v] of Object.entries(obj.$associations)) {
|
||||
this.stripHash(v)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
private translateKey<T extends Doc>(
|
||||
key: string,
|
||||
clazz: Ref<Class<T>>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/postgres",
|
||||
"version": "0.7.22",
|
||||
"version": "0.7.23",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
@@ -26,20 +26,20 @@
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/node": "^22.18.1",
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"postgres": "^3.4.7",
|
||||
"@hcengineering/core": "^0.7.23",
|
||||
"@hcengineering/platform": "^0.7.19",
|
||||
"@hcengineering/server-core": "workspace:^0.7.18",
|
||||
"@hcengineering/postgres-base": "^0.7.17"
|
||||
"@hcengineering/core": "^0.7.26",
|
||||
"@hcengineering/platform": "^0.7.20",
|
||||
"@hcengineering/server-core": "workspace:^0.7.19",
|
||||
"@hcengineering/postgres-base": "^0.7.18"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/huly.server",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -385,6 +385,57 @@ describe('postgres operations', () => {
|
||||
}
|
||||
)
|
||||
expect(r.length).toEqual(1)
|
||||
expect((r[0].$associations?.[association._id][0] as unknown as Task)?._id).toEqual(secondTask)
|
||||
expect((r[0].$associations?.[association._id + '_b'][0] as unknown as Task)?._id).toEqual(secondTask)
|
||||
})
|
||||
|
||||
it('check deep associations', async () => {
|
||||
const association = await operations.findOne(core.class.Association, {})
|
||||
if (association == null) {
|
||||
throw new Error('Association not found')
|
||||
}
|
||||
|
||||
const firstTask = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
|
||||
name: 'my-task',
|
||||
description: 'Descr',
|
||||
rate: 20
|
||||
})
|
||||
|
||||
const secondTask = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
|
||||
name: 'my-task2',
|
||||
description: 'Descr',
|
||||
rate: 20
|
||||
})
|
||||
|
||||
const thirdTask = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
|
||||
name: 'my-task2',
|
||||
description: 'Descr',
|
||||
rate: 20
|
||||
})
|
||||
|
||||
await operations.createDoc(core.class.Relation, '' as Ref<Space>, {
|
||||
docA: firstTask,
|
||||
docB: secondTask,
|
||||
association: association._id
|
||||
})
|
||||
|
||||
await operations.createDoc(core.class.Relation, '' as Ref<Space>, {
|
||||
docA: secondTask,
|
||||
docB: thirdTask,
|
||||
association: association._id
|
||||
})
|
||||
|
||||
const r = await client.findAll(
|
||||
taskPlugin.class.Task,
|
||||
{ _id: firstTask },
|
||||
{
|
||||
associations: [[association._id, 1, [[association._id, 1]]]]
|
||||
}
|
||||
)
|
||||
expect(r.length).toEqual(1)
|
||||
expect((r[0].$associations?.[`${association._id}_b`][0] as unknown as Task)?._id).toEqual(secondTask)
|
||||
expect(
|
||||
(r[0].$associations?.[`${association._id}_b`][0]?.$associations?.[`${association._id}_b`][0] as unknown as Task)
|
||||
?._id
|
||||
).toEqual(thirdTask)
|
||||
})
|
||||
})
|
||||
|
||||
+178
-157
@@ -100,7 +100,6 @@ import {
|
||||
DBCollectionHelper,
|
||||
type DBDoc,
|
||||
escape,
|
||||
filterProjection,
|
||||
inferType,
|
||||
isDataField,
|
||||
isOwner,
|
||||
@@ -109,7 +108,8 @@ import {
|
||||
parseDoc,
|
||||
parseDocWithProjection,
|
||||
parseUpdate,
|
||||
simpleEscape
|
||||
simpleEscape,
|
||||
toWithLookup
|
||||
} from './utils'
|
||||
async function * createCursorGenerator (
|
||||
client: postgres.Sql,
|
||||
@@ -296,7 +296,7 @@ abstract class PostgresAdapterBase implements DbAdapter {
|
||||
async rawFindAll<T extends Doc>(_domain: Domain, query: DocumentQuery<T>, options?: FindOptions<T>): Promise<T[]> {
|
||||
const domain = translateDomain(_domain)
|
||||
const vars = new ValuesVariables()
|
||||
const select = `SELECT ${this.getProjection(vars, domain, options?.projection, [], options?.associations)} FROM ${domain}`
|
||||
const select = `SELECT ${this.getProjection(vars, domain, options?.projection, [])} FROM ${domain}`
|
||||
const sqlChunks: string[] = []
|
||||
sqlChunks.push(`WHERE ${this.buildRawQuery(vars, domain, query, options)}`)
|
||||
if (options?.sort !== undefined) {
|
||||
@@ -472,7 +472,7 @@ abstract class PostgresAdapterBase implements DbAdapter {
|
||||
|
||||
const projection = this.localizeProjection(_class, options?.projection ?? undefined)
|
||||
|
||||
const select = `SELECT ${this.getProjection(vars, domain, projection, joins, options?.associations)} FROM ${domain}`
|
||||
const select = `SELECT ${this.getProjection(vars, domain, projection, joins)} FROM ${domain}`
|
||||
|
||||
if (joins.length > 0) {
|
||||
sqlChunks.push(this.buildJoinString(vars, joins))
|
||||
@@ -554,7 +554,7 @@ abstract class PostgresAdapterBase implements DbAdapter {
|
||||
total
|
||||
)
|
||||
} else {
|
||||
const res = this.parseLookup<T>(result, joins, projection, domain)
|
||||
const res = await this.parseLookup<T>(ctx, result, joins, projection, options.associations, domain)
|
||||
return toFindResult(res, total)
|
||||
}
|
||||
})) as FindResult<T>
|
||||
@@ -638,153 +638,208 @@ abstract class PostgresAdapterBase implements DbAdapter {
|
||||
const res = `EXISTS (SELECT 1 FROM ${translateDomain(DOMAIN_SPACE)} sec WHERE sec._id = ${domain}.${key} AND sec."workspaceId" = ${vars.add(this.workspaceId, '::uuid')} AND ${q})`
|
||||
|
||||
const collabSec = getClassCollaborators(this.modelDb, this.hierarchy, _class)
|
||||
if (collabSec?.provideSecurity === true && [AccountRole.Guest, AccountRole.ReadOnlyGuest].includes(acc.role)) {
|
||||
const collab = `OR EXISTS (SELECT 1 FROM ${translateDomain(DOMAIN_COLLABORATOR)} collab_sec WHERE collab_sec."workspaceId" = ${vars.add(this.workspaceId, '::uuid')} AND collab_sec."attachedTo" = ${domain}._id AND collab_sec.collaborator = '${acc.uuid}')`
|
||||
return `AND (${res} ${collab})`
|
||||
let collabRes = ''
|
||||
if ([AccountRole.Guest, AccountRole.ReadOnlyGuest].includes(acc.role)) {
|
||||
if (collabSec?.provideSecurity === true) {
|
||||
collabRes += ` OR EXISTS (SELECT 1 FROM ${translateDomain(DOMAIN_COLLABORATOR)} collab_sec WHERE collab_sec."workspaceId" = ${vars.add(this.workspaceId, '::uuid')} AND collab_sec."attachedTo" = ${domain}._id AND collab_sec.collaborator = '${acc.uuid}')`
|
||||
}
|
||||
if (collabSec?.provideAttachedSecurity === true) {
|
||||
collabRes += ` OR EXISTS (SELECT 1 FROM ${translateDomain(DOMAIN_COLLABORATOR)} collab_sec WHERE collab_sec."workspaceId" = ${vars.add(this.workspaceId, '::uuid')} AND collab_sec."attachedTo" = ${domain}."attachedTo" AND collab_sec.collaborator = '${acc.uuid}')`
|
||||
}
|
||||
}
|
||||
return `AND (${res})`
|
||||
return `AND (${res}${collabRes})`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private parseLookup<T extends Doc>(
|
||||
private async parseLookup<T extends Doc>(
|
||||
ctx: MeasureContext<SessionData>,
|
||||
rows: any[],
|
||||
joins: JoinProps[],
|
||||
projection: Projection<T> | undefined,
|
||||
associations: AssociationQuery[] | undefined,
|
||||
domain: string
|
||||
): WithLookup<T>[] {
|
||||
): Promise<WithLookup<T>[]> {
|
||||
const map = new Map<Ref<T>, WithLookup<T>>()
|
||||
|
||||
const modelJoins: JoinProps[] = []
|
||||
const reverseJoins: JoinProps[] = []
|
||||
const simpleJoins: JoinProps[] = []
|
||||
|
||||
for (const join of joins) {
|
||||
if (join.table === DOMAIN_MODEL) {
|
||||
modelJoins.push(join)
|
||||
} else if (join.isReverse) {
|
||||
reverseJoins.push(join)
|
||||
} else if (join.path !== '') {
|
||||
simpleJoins.push(join)
|
||||
}
|
||||
if (join.table === DOMAIN_MODEL) modelJoins.push(join)
|
||||
else if (join.isReverse) reverseJoins.push(join)
|
||||
else if (join.path !== '') simpleJoins.push(join)
|
||||
}
|
||||
|
||||
for (const row of rows) {
|
||||
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
||||
let doc: WithLookup<T> = map.get(row._id) ?? ({ _id: row._id, $lookup: {}, $associations: {} } as WithLookup<T>)
|
||||
const associations: Record<string, any> = doc.$associations as Record<string, any>
|
||||
const lookup: Record<string, any> = doc.$lookup as Record<string, any>
|
||||
let joinIndex: number | undefined
|
||||
let skip = false
|
||||
try {
|
||||
const schema = getSchema(domain)
|
||||
for (const column in row) {
|
||||
if (column.startsWith('reverse_lookup_')) {
|
||||
if (row[column] != null) {
|
||||
const join = reverseJoins.find((j) => j.toAlias.toLowerCase() === column)
|
||||
if (join === undefined) {
|
||||
continue
|
||||
}
|
||||
const res = this.getLookupValue(join.path, lookup, false)
|
||||
if (res === undefined) continue
|
||||
const { obj, key } = res
|
||||
const doc = toWithLookup<T>(parseDoc(row, getSchema(row._class)))
|
||||
|
||||
const parsed = row[column].map((p: any) => parseDoc(p, schema))
|
||||
obj[key] = parsed
|
||||
}
|
||||
} else if (column.startsWith('lookup_')) {
|
||||
const keys = column.split('_')
|
||||
let key = keys[keys.length - 1]
|
||||
if (keys[keys.length - 2] === '') {
|
||||
key = '_' + key
|
||||
}
|
||||
const lookup = doc.$lookup as Record<string, any>
|
||||
|
||||
if (key === 'workspaceId') {
|
||||
continue
|
||||
}
|
||||
this.parseLookupColumns(row, simpleJoins, reverseJoins, lookup, domain)
|
||||
|
||||
if (key === '_id') {
|
||||
joinIndex = joinIndex === undefined ? 0 : ++joinIndex
|
||||
if (row[column] === null) {
|
||||
skip = true
|
||||
continue
|
||||
}
|
||||
skip = false
|
||||
}
|
||||
|
||||
if (skip) {
|
||||
continue
|
||||
}
|
||||
|
||||
const join = simpleJoins[joinIndex ?? 0]
|
||||
if (join === undefined) {
|
||||
continue
|
||||
}
|
||||
const res = this.getLookupValue(join.path, lookup)
|
||||
if (res === undefined) continue
|
||||
const { obj, key: p } = res
|
||||
|
||||
if (key === 'data') {
|
||||
obj[p] = { ...obj[p], ...row[column] }
|
||||
} else {
|
||||
if (key === 'createdOn' || key === 'modifiedOn') {
|
||||
const val = Number.parseInt(row[column])
|
||||
obj[p][key] = Number.isNaN(val) ? null : val
|
||||
} else if (key === '%hash%') {
|
||||
continue
|
||||
} else if (key === 'attachedTo' && row[column] === 'NULL') {
|
||||
continue
|
||||
} else {
|
||||
obj[p][key] = row[column] === 'NULL' ? null : row[column]
|
||||
}
|
||||
}
|
||||
} else if (column.startsWith('assoc_')) {
|
||||
if (row[column] == null) continue
|
||||
const keys = column.split('_')
|
||||
const key = keys[keys.length - 1]
|
||||
const associationDomain = keys[1]
|
||||
const associationSchema = getSchema(associationDomain)
|
||||
const parsed = row[column].map((p: any) => parseDoc(p, associationSchema))
|
||||
associations[key] = parsed
|
||||
} else {
|
||||
joinIndex = undefined
|
||||
if (!map.has(row._id)) {
|
||||
if (column === 'workspaceId') {
|
||||
continue
|
||||
}
|
||||
if (column === 'data') {
|
||||
let data = row[column]
|
||||
data = filterProjection(data, projection)
|
||||
doc = { ...doc, ...data }
|
||||
} else {
|
||||
if (column === 'createdOn' || column === 'modifiedOn') {
|
||||
const val = Number.parseInt(row[column])
|
||||
;(doc as any)[column] = Number.isNaN(val) ? null : val
|
||||
} else if (column === '%hash%') {
|
||||
// Ignore
|
||||
} else {
|
||||
;(doc as any)[column] = row[column] === 'NULL' ? null : row[column]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
throw err
|
||||
}
|
||||
for (const modelJoin of modelJoins) {
|
||||
const res = this.getLookupValue(modelJoin.path, lookup)
|
||||
if (res === undefined) continue
|
||||
|
||||
const { obj, key } = res
|
||||
const val = this.getModelLookupValue<T>(doc, modelJoin, [...simpleJoins, ...modelJoins])
|
||||
|
||||
if (val !== undefined && modelJoin.toClass !== undefined) {
|
||||
const res = this.modelDb.findAllSync(modelJoin.toClass, {
|
||||
const res2 = this.modelDb.findAllSync(modelJoin.toClass, {
|
||||
[modelJoin.toField]: val
|
||||
})
|
||||
obj[key] = modelJoin.isReverse ? res : res[0]
|
||||
obj[key] = modelJoin.isReverse ? res2 : res2[0]
|
||||
}
|
||||
}
|
||||
|
||||
map.set(row._id, doc)
|
||||
}
|
||||
return Array.from(map.values())
|
||||
|
||||
if (associations !== undefined && map.size > 0) {
|
||||
await this.fetchAssociations(ctx, map, associations)
|
||||
}
|
||||
|
||||
return [...map.values()]
|
||||
}
|
||||
|
||||
async fetchAssociations (
|
||||
ctx: MeasureContext<SessionData>,
|
||||
parentMap: Map<string, WithLookup<Doc>>,
|
||||
associations: AssociationQuery[]
|
||||
): Promise<void> {
|
||||
for (const association of associations) {
|
||||
const [assocId, dir, nested] = association
|
||||
const isReverse = dir === -1
|
||||
const keyA = isReverse ? 'docB' : 'docA'
|
||||
const keyB = isReverse ? 'docA' : 'docB'
|
||||
|
||||
const assoc = this.modelDb.findObject(assocId)
|
||||
if (assoc === undefined) {
|
||||
continue
|
||||
}
|
||||
const _class = isReverse ? assoc.classA : assoc.classB
|
||||
const domain = this.hierarchy.findDomain(_class)
|
||||
if (domain === undefined) continue
|
||||
const tagetDomain = translateDomain(domain)
|
||||
|
||||
const vars = new ValuesVariables()
|
||||
|
||||
const wsId = vars.add(this.workspaceId, '::uuid')
|
||||
const parentIds = vars.add(Array.from(parentMap.keys()), '::text[]')
|
||||
const assocIdVar = vars.add(assocId)
|
||||
|
||||
const rows = await this.mgr.retry(ctx.id, this.mgrId, async (connection) => {
|
||||
return await connection.execute(
|
||||
`
|
||||
SELECT assoc.*, r."${keyA}" as parent_id
|
||||
FROM ${tagetDomain} AS assoc
|
||||
JOIN ${translateDomain(DOMAIN_RELATION)} AS r
|
||||
ON r."${keyB}" = assoc."_id"
|
||||
WHERE r."${keyA}" = ANY(${parentIds})
|
||||
AND r.association = ${assocIdVar}
|
||||
AND r."workspaceId" = ${wsId}
|
||||
AND assoc."workspaceId" = ${wsId}
|
||||
`,
|
||||
vars.getValues()
|
||||
)
|
||||
})
|
||||
|
||||
const key = `${assocId}_${!isReverse ? 'b' : 'a'}`
|
||||
|
||||
const nextParentMap = new Map<string, WithLookup<Doc>>()
|
||||
for (const row of rows) {
|
||||
const parentId = row.parent_id
|
||||
const parsed = nextParentMap.get(row._id) ?? parseDoc(row, getSchema(row._class))
|
||||
|
||||
const parent = parentMap.get(parentId)
|
||||
if (parent === undefined) continue
|
||||
|
||||
if (parent.$associations === undefined) {
|
||||
parent.$associations = {}
|
||||
}
|
||||
|
||||
if (parent.$associations[key] === undefined) parent.$associations[key] = []
|
||||
parent.$associations[key].push(parsed)
|
||||
if (!nextParentMap.has(parsed._id)) {
|
||||
nextParentMap.set(parsed._id, parsed)
|
||||
}
|
||||
}
|
||||
|
||||
if (nested !== undefined && nested.length > 0 && nextParentMap.size > 0) {
|
||||
await this.fetchAssociations(ctx, nextParentMap, nested)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private parseLookupColumns (
|
||||
row: any,
|
||||
simpleJoins: JoinProps[],
|
||||
reverseJoins: JoinProps[],
|
||||
lookup: Record<string, any>,
|
||||
domain: string
|
||||
): void {
|
||||
const schema = getSchema(domain)
|
||||
let joinIndex: number | undefined
|
||||
let skip = false
|
||||
|
||||
for (const column in row) {
|
||||
if (column.startsWith('reverse_lookup_')) {
|
||||
const join = reverseJoins.find((j) => j.toAlias.toLowerCase() === column)
|
||||
if (join === undefined || row[column] == null) continue
|
||||
|
||||
const parsed = row[column].map((p: any) => parseDoc(p, schema))
|
||||
|
||||
const res = this.getLookupValue(join.path, lookup, false)
|
||||
if (res === undefined) continue
|
||||
const { obj, key } = res
|
||||
obj[key] = parsed
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (column.startsWith('lookup_')) {
|
||||
const keys = column.split('_')
|
||||
let key = keys[keys.length - 1]
|
||||
if (keys[keys.length - 2] === '') {
|
||||
key = '_' + key
|
||||
}
|
||||
|
||||
if (key === 'workspaceId') continue
|
||||
|
||||
if (key === '_id') {
|
||||
joinIndex = joinIndex === undefined ? 0 : joinIndex + 1
|
||||
if (row[column] == null) {
|
||||
skip = true
|
||||
continue
|
||||
}
|
||||
skip = false
|
||||
}
|
||||
|
||||
if (skip) continue
|
||||
|
||||
const join = simpleJoins[joinIndex ?? 0]
|
||||
if (join === undefined) continue
|
||||
|
||||
const res = this.getLookupValue(join.path, lookup)
|
||||
if (res === undefined) continue
|
||||
const { obj, key: p } = res
|
||||
|
||||
if (key === 'data') {
|
||||
obj[p] = { ...obj[p], ...row[column] }
|
||||
} else if (key === 'createdOn' || key === 'modifiedOn') {
|
||||
const val = parseInt(row[column])
|
||||
obj[p][key] = Number.isNaN(val) ? null : val
|
||||
} else if (key === '%hash%') {
|
||||
// ignore
|
||||
} else if (key === 'attachedTo' && row[column] === 'NULL') {
|
||||
// ignore
|
||||
} else {
|
||||
obj[p][key] = row[column] === 'NULL' ? null : row[column]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getLookupValue (
|
||||
@@ -1399,36 +1454,6 @@ abstract class PostgresAdapterBase implements DbAdapter {
|
||||
return res
|
||||
}
|
||||
|
||||
getAssociationsProjections (vars: ValuesVariables, baseDomain: string, associations: AssociationQuery[]): string[] {
|
||||
const res: string[] = []
|
||||
for (const association of associations) {
|
||||
const _id = escape(association[0])
|
||||
const assoc = this.modelDb.findObject(_id)
|
||||
if (assoc === undefined) {
|
||||
continue
|
||||
}
|
||||
const isReverse = association[1] === -1
|
||||
const _class = isReverse ? assoc.classA : assoc.classB
|
||||
const domain = this.hierarchy.findDomain(_class)
|
||||
if (domain === undefined) continue
|
||||
const tagetDomain = translateDomain(domain)
|
||||
const keyA = isReverse ? 'docB' : 'docA'
|
||||
const keyB = isReverse ? 'docA' : 'docB'
|
||||
const wsId = vars.add(this.workspaceId, '::uuid')
|
||||
res.push(
|
||||
`(SELECT jsonb_agg(assoc.*)
|
||||
FROM ${tagetDomain} AS assoc
|
||||
JOIN ${translateDomain(DOMAIN_RELATION)} as relation
|
||||
ON relation."${keyB}" = assoc."_id"
|
||||
AND relation."workspaceId" = ${wsId}
|
||||
WHERE relation."${keyA}" = ${translateDomain(baseDomain)}."_id"
|
||||
AND relation.association = '${_id}'
|
||||
AND assoc."workspaceId" = ${wsId}) AS assoc_${tagetDomain}_${_id}`
|
||||
)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@withContext('get-domain-hash')
|
||||
async getDomainHash (ctx: MeasureContext, domain: Domain): Promise<string> {
|
||||
return await calcHashHash(ctx, domain, this)
|
||||
@@ -1442,10 +1467,9 @@ abstract class PostgresAdapterBase implements DbAdapter {
|
||||
vars: ValuesVariables,
|
||||
baseDomain: string,
|
||||
projection: Projection<T> | undefined,
|
||||
joins: JoinProps[],
|
||||
associations: AssociationQuery[] | undefined
|
||||
joins: JoinProps[]
|
||||
): string | '*' {
|
||||
if (projection === undefined && joins.length === 0 && associations === undefined) return `${baseDomain}.*`
|
||||
if (projection === undefined && joins.length === 0) return `${baseDomain}.*`
|
||||
const res: string[] = []
|
||||
let dataAdded = false
|
||||
if (projection === undefined) {
|
||||
@@ -1472,9 +1496,6 @@ abstract class PostgresAdapterBase implements DbAdapter {
|
||||
for (const join of joins) {
|
||||
res.push(...this.getProjectionsAliases(vars, join))
|
||||
}
|
||||
if (associations !== undefined) {
|
||||
res.push(...this.getAssociationsProjections(vars, baseDomain, associations))
|
||||
}
|
||||
return res.join(', ')
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import core, {
|
||||
type Projection,
|
||||
type Ref,
|
||||
systemAccountUuid,
|
||||
type WithLookup,
|
||||
type WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
import { type DomainHelperOperations } from '@hcengineering/server-core'
|
||||
@@ -473,6 +474,17 @@ export function parseDocWithProjection<T extends Doc> (
|
||||
return res
|
||||
}
|
||||
|
||||
export function toWithLookup<T extends Doc> (doc: T): WithLookup<T> {
|
||||
const res = doc as WithLookup<T>
|
||||
if (res.$associations === undefined) {
|
||||
res.$associations = {}
|
||||
}
|
||||
if (res.$lookup === undefined) {
|
||||
res.$lookup = {}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
export function parseDoc<T extends Doc> (doc: DBDoc, schema: Schema): T {
|
||||
const { workspaceId, data, ...rest } = doc
|
||||
for (const key in rest) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/s3",
|
||||
"version": "0.7.17",
|
||||
"version": "0.7.18",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
@@ -26,19 +26,19 @@
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/node": "^22.18.1",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "^0.7.23",
|
||||
"@hcengineering/platform": "^0.7.19",
|
||||
"@hcengineering/server-core": "workspace:^0.7.18",
|
||||
"@hcengineering/storage": "^0.7.17",
|
||||
"@hcengineering/core": "^0.7.26",
|
||||
"@hcengineering/platform": "^0.7.20",
|
||||
"@hcengineering/server-core": "workspace:^0.7.19",
|
||||
"@hcengineering/storage": "^0.7.18",
|
||||
"@aws-sdk/client-s3": "^3.738.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.738.0",
|
||||
"@aws-sdk/lib-storage": "^3.738.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/server-storage",
|
||||
"version": "0.7.17",
|
||||
"version": "0.7.18",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
@@ -39,16 +39,16 @@
|
||||
"devDependencies": {
|
||||
"cross-env": "~7.0.3",
|
||||
"@hcengineering/platform-rig": "^0.7.19",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/node": "^22.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint": "^8.54.0",
|
||||
"ts-node": "^10.8.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
@@ -56,16 +56,16 @@
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "^0.7.23",
|
||||
"@hcengineering/platform": "^0.7.19",
|
||||
"@hcengineering/server-core": "workspace:^0.7.18",
|
||||
"@hcengineering/minio": "workspace:^0.7.17",
|
||||
"@hcengineering/s3": "workspace:^0.7.17",
|
||||
"@hcengineering/datalake": "workspace:^0.7.17",
|
||||
"@hcengineering/hulylake": "workspace:^0.7.17",
|
||||
"@hcengineering/storage": "^0.7.17",
|
||||
"@hcengineering/analytics": "^0.7.17",
|
||||
"@hcengineering/server-token": "^0.7.17"
|
||||
"@hcengineering/core": "^0.7.26",
|
||||
"@hcengineering/platform": "^0.7.20",
|
||||
"@hcengineering/server-core": "workspace:^0.7.19",
|
||||
"@hcengineering/minio": "workspace:^0.7.18",
|
||||
"@hcengineering/s3": "workspace:^0.7.18",
|
||||
"@hcengineering/datalake": "workspace:^0.7.18",
|
||||
"@hcengineering/hulylake": "workspace:^0.7.18",
|
||||
"@hcengineering/storage": "^0.7.18",
|
||||
"@hcengineering/analytics": "^0.7.19",
|
||||
"@hcengineering/server-token": "^0.7.18"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/huly.server",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/server",
|
||||
"version": "0.7.18",
|
||||
"version": "0.7.19",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
@@ -20,7 +20,7 @@
|
||||
"devDependencies": {
|
||||
"cross-env": "~7.0.3",
|
||||
"@hcengineering/platform-rig": "^0.7.19",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/node": "^22.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
@@ -28,7 +28,7 @@
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
@@ -36,13 +36,13 @@
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/account-client": "^0.7.20",
|
||||
"@hcengineering/analytics": "^0.7.17",
|
||||
"@hcengineering/core": "^0.7.23",
|
||||
"@hcengineering/platform": "^0.7.19",
|
||||
"@hcengineering/rpc": "^0.7.17",
|
||||
"@hcengineering/server-core": "workspace:^0.7.18",
|
||||
"@hcengineering/server-token": "^0.7.17",
|
||||
"@hcengineering/account-client": "^0.7.25",
|
||||
"@hcengineering/analytics": "^0.7.19",
|
||||
"@hcengineering/core": "^0.7.26",
|
||||
"@hcengineering/platform": "^0.7.20",
|
||||
"@hcengineering/rpc": "^0.7.18",
|
||||
"@hcengineering/server-core": "workspace:^0.7.19",
|
||||
"@hcengineering/server-token": "^0.7.18",
|
||||
"utf-8-validate": "^6.0.4"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/huly.server",
|
||||
|
||||
Reference in New Issue
Block a user