Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/platform/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * from './event'
export * from './i18n'
export * from './metadata'
export * from './platform'
export * from './ident'
export { default } from './platform'
export * from './resource'
export * from './status'
Expand Down
2 changes: 2 additions & 0 deletions plugins/process-resources/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
"build:docs": "api-extractor run --local",
"format": "format src",
"svelte-check": "do-svelte-check",
"test": "jest --passWithNoTests --silent",
"_phase:svelte-check": "do-svelte-check",
"build:watch": "compile ui",
"_phase:build": "compile ui",
"_phase:test": "jest --passWithNoTests --silent",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
Expand Down
12 changes: 10 additions & 2 deletions plugins/process-resources/src/components/ProcessAttribute.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<script lang="ts">
import { MasterTag, Tag } from '@hcengineering/card'
import { AnyAttribute, Class, Doc, Ref } from '@hcengineering/core'
import { Context, parseContext, Process, SelectedContext } from '@hcengineering/process'
import { Context, createDSLContext, parseContext, Process, SelectedContext } from '@hcengineering/process'
import {
AnySvelteComponent,
Button,
Expand Down Expand Up @@ -69,7 +69,15 @@
}

function onSelect (res: SelectedContext | null): void {
value = res === null ? undefined : '$' + JSON.stringify(res)
if (res === null) {
value = undefined
} else {
try {
value = createDSLContext(res)
} catch {
value = '$' + JSON.stringify(res)
}
}
dispatch('change', value)
}
</script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { AnyAttribute, Class, Doc, DocumentQuery, Ref } from '@hcengineering/core'
import core, { AnyAttribute, Class, Doc, DocumentQuery, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Context, Func, Process, ProcessFunction, SelectedContext } from '@hcengineering/process'
import {
Expand Down Expand Up @@ -64,11 +64,22 @@
}
}

const reduceFuncs = client
.getModel()
.findAllSync(plugin.class.ProcessFunction, { type: 'reduce' })
.filter((p) => p.category === undefined || (allowArray && p.category === 'array'))
.map((it) => it._id)
function getReduceFunctions (): Ref<ProcessFunction>[] {
const model = client.getModel()
const h = client.getHierarchy()
const res: Ref<ProcessFunction>[] = []
const all = model.findAllSync(plugin.class.ProcessFunction, { type: 'reduce' })
for (const f of all) {
if (f.category === undefined || (allowArray && f.category === 'array')) {
if (f.of === core.class.ArrOf || h.isDerived(f.of, attrClass)) {
res.push(f._id)
}
}
}
return res
}

const reduceFuncs = getReduceFunctions()

$: availableFunctions = getAvailableFunctions(contextValue.functions, attrClass, category)

Expand Down
57 changes: 57 additions & 0 deletions plugins/process/docs/dsl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
Process DSL — context expressions

This document describes the small DSL used to reference parts of a process execution context and to apply "modifiers" (functions, source reducers, fallbacks) to those references. The implementation lives in `src/dslContext.ts` and the tests are at `src/__tests__/dslContext.test.ts`.

Overview
--------

A DSL expression is always enclosed in `${...}` when embedded. The parser and serializer in this package support converting between the typed `SelectedContext` objects and their compact DSL string.

Top-level expression types
-------------------------

- Attribute: `@<key>` — references an attribute on the current object.
- Example: `@name`

- Nested attribute: `@<refKey>.<attrKey>` — follows a reference attribute to another object and takes its attribute.
- Example: `@owner.id`

- Relation: `$relation(<associationRef>,<direction>,<key>,<name>)`
- Example: `$relation(my.assoc, A, id, title)`

- Execution context: `$context(<id>,<key>)`
- Example: `$context(12345, result)`

- User request: `$userRequest(<id>,<key>,<_class>)`
- Example: `$userRequest(123, value, MyClass)`

- Const: `#<value>,<key>` — a literal value and a target key.
- Numbers, booleans, `null`/`undefined`, quoted strings and arrays are supported.
- Example: `#42,answer` or `#"hello",greeting` or `#[1,2,3],arr`

- Function as primary expression: `=><funcRef>(<props...>)`
- Example: `=>myFunc(a=1,b="x")`

Modifiers
---------

Modifiers always start with `=>` and are appended after the main expression separated by spaces (at top-level). Examples:

- Source reducer: `=>SOURCE(<funcRef>,<props...>)`
- Fallback: `=>FALLBACK(<props...>)` or `=>FALLBACK(<value>)`
- Arbitrary function modifiers: `=><funcRef>(<props...>)`

Important parsing details
-------------------------

- Splitting into expression + modifiers is done at top-level only — the parser ignores `=>` inside nested brackets, quotes and template expressions `${...}`.
- The tokenizer respects nested parentheses `()`, arrays `[]`, object braces `{}`, quoted strings (single/double) with escapes, and `${...}` template blocks.
- `simplifyFuncRef` is used when serializing to shorten function references; `expandFuncRef` restores those during parsing.

Examples
--------

- `${@name}` — attribute `name`
- `${@owner.id =>MYFUNC(a=1) =>FALLBACK(10)}` — nested attribute with a function modifier and a fallback
- `${#"he\"llo",greet}` — const string with escaped quote
- `${=>COMPUTE(sum=1)}` — function as primary expression
71 changes: 71 additions & 0 deletions plugins/process/src/__tests__/dslContext.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { createDSLContext, parseDSLContext } from '../dslContext'

describe('dslContext roundtrip', () => {
test('attribute', () => {
const original = { type: 'attribute', key: 'name' } as any
const dsl = createDSLContext(original)
const parsed = parseDSLContext(dsl)
expect(parsed).toEqual(expect.objectContaining({ type: 'attribute', key: 'name' }))
})

test('nested', () => {
const original = { type: 'nested', path: 'owner', key: 'id' } as any
const dsl = createDSLContext(original)
const parsed = parseDSLContext(dsl)
expect(parsed).toEqual(expect.objectContaining({ type: 'nested', path: 'owner', key: 'id' }))
})

test('const number and string', () => {
const originalNum = { type: 'const', value: 42, key: 'answer' } as any
const dslNum = createDSLContext(originalNum)
const parsedNum = parseDSLContext(dslNum)
expect(parsedNum).toEqual(expect.objectContaining({ type: 'const', key: 'answer', value: 42 }))

const originalStr = { type: 'const', value: 'he"llo', key: 'greet' } as any
const dslStr = createDSLContext(originalStr)
const parsedStr = parseDSLContext(dslStr)
expect(parsedStr).toEqual(expect.objectContaining({ type: 'const', key: 'greet', value: 'he"llo' }))
})

test('array const', () => {
const original = { type: 'const', value: [1, 2, 3], key: 'arr' } as any
const dsl = createDSLContext(original)
const parsed = parseDSLContext(dsl)
expect(parsed).toEqual(expect.objectContaining({ type: 'const', key: 'arr', value: [1, 2, 3] }))
})

test('function as primary', () => {
const original = { type: 'function', func: 'myFunc' as any, props: { a: 1 }, key: '' } as any
const dsl = createDSLContext(original)
const parsed = parseDSLContext(dsl)
expect((parsed as any).type).toBe('function')
expect((parsed as any).func).toBeDefined()
})

test('modifiers SOURCE and FALLBACK and extra function', () => {
const original = {
type: 'attribute',
key: 'x',
sourceFunction: { func: 'src' as any, props: { p: 'v' } },
functions: [{ func: 'f1' as any, props: {} }],
fallbackValue: 10
} as any
const dsl = createDSLContext(original)
const parsed = parseDSLContext(dsl)
expect(parsed).toBeDefined()
expect((parsed as any).sourceFunction).toBeDefined()
expect((parsed as any).fallbackValue).toBe(10)
expect((parsed as any).functions?.length).toBeGreaterThanOrEqual(1)
})

test('nested template with arrow inside should not split modifiers', () => {
// eslint-disable-next-line no-template-curly-in-string
const dsl = '${@x=>MYFUNC(${@inner=>OTHER()})=>FALLBACK(1)}'
const parsed = parseDSLContext(dsl)
expect(parsed).toBeDefined()
expect((parsed as any).type).toBe('attribute')
expect((parsed as any).key).toBe('x')
expect((parsed as any).functions?.length).toBeGreaterThanOrEqual(1)
expect((parsed as any).fallbackValue).toBe(1)
})
})
Loading