@contember/schema-definition 1.3.0-alpha.2 → 1.3.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/actions/definition/index.d.ts +5 -0
- package/dist/src/actions/definition/index.d.ts.map +1 -0
- package/dist/src/actions/definition/index.js +26 -0
- package/dist/src/actions/definition/index.js.map +1 -0
- package/dist/src/actions/definition/internal/ActionsFactory.d.ts +11 -0
- package/dist/src/actions/definition/internal/ActionsFactory.d.ts.map +1 -0
- package/dist/src/actions/definition/internal/ActionsFactory.js +182 -0
- package/dist/src/actions/definition/internal/ActionsFactory.js.map +1 -0
- package/dist/src/actions/definition/internal/store.d.ts +11 -0
- package/dist/src/actions/definition/internal/store.d.ts.map +1 -0
- package/dist/src/actions/definition/internal/store.js +6 -0
- package/dist/src/actions/definition/internal/store.js.map +1 -0
- package/dist/src/actions/definition/targets.d.ts +10 -0
- package/dist/src/actions/definition/targets.d.ts.map +1 -0
- package/dist/src/actions/definition/targets.js +13 -0
- package/dist/src/actions/definition/targets.js.map +1 -0
- package/dist/src/actions/definition/triggers.d.ts +27 -0
- package/dist/src/actions/definition/triggers.d.ts.map +1 -0
- package/dist/src/actions/definition/triggers.js +13 -0
- package/dist/src/actions/definition/triggers.js.map +1 -0
- package/dist/src/createSchema.d.ts.map +1 -1
- package/dist/src/createSchema.js +3 -1
- package/dist/src/createSchema.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/dist/tests/unit/readActions.test.d.ts +2 -0
- package/dist/tests/unit/readActions.test.d.ts.map +1 -0
- package/dist/tests/unit/readActions.test.js +107 -0
- package/dist/tests/unit/readActions.test.js.map +1 -0
- package/package.json +13 -4
- package/src/actions/definition/index.ts +13 -0
- package/src/actions/definition/internal/ActionsFactory.ts +190 -0
- package/src/actions/definition/internal/store.ts +8 -0
- package/src/actions/definition/targets.ts +15 -0
- package/src/actions/definition/triggers.ts +45 -0
- package/src/createSchema.ts +3 -1
- package/src/index.ts +1 -0
- package/tests/unit/readActions.test.ts +83 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { assertNever, filterEntityDefinition } from '../../../utils'
|
|
2
|
+
import { Actions } from '@contember/schema'
|
|
3
|
+
import { triggersStore } from './store'
|
|
4
|
+
import { BasicTriggerDefinition, TargetDefinition, WatchTriggerDefinition } from '../triggers'
|
|
5
|
+
import { ActionsTarget } from '../targets'
|
|
6
|
+
import { Kind, parse, SelectionNode, ValueNode } from 'graphql'
|
|
7
|
+
|
|
8
|
+
export class ActionsFactory {
|
|
9
|
+
constructor(
|
|
10
|
+
) {
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public create(
|
|
14
|
+
exportedDefinitions: Record<string, any>,
|
|
15
|
+
): Actions.Schema {
|
|
16
|
+
const entityLikeDefinition = filterEntityDefinition(exportedDefinitions)
|
|
17
|
+
|
|
18
|
+
const targetsRegistry = new ActionsTargetRegistry()
|
|
19
|
+
for (const [exportedAs, def] of Object.entries(exportedDefinitions)) {
|
|
20
|
+
if (!(def instanceof ActionsTarget)) {
|
|
21
|
+
continue
|
|
22
|
+
}
|
|
23
|
+
const name = def.name ?? exportedAs
|
|
24
|
+
targetsRegistry.register(name, def.definition)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const triggersDef = entityLikeDefinition.flatMap(([, entity]) => triggersStore.get(entity).map(it => ({
|
|
28
|
+
...it,
|
|
29
|
+
entity,
|
|
30
|
+
})))
|
|
31
|
+
|
|
32
|
+
const triggerNames = triggersDef.map(it => it.definition.name).filter((it): it is string => !!it)
|
|
33
|
+
triggerNames.forEach((it, index, arr) => {
|
|
34
|
+
if (arr.indexOf(it) !== index) {
|
|
35
|
+
throw `Duplicate trigger name ${it}.`
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const triggers: Record<string, Actions.AnyTrigger> = {}
|
|
40
|
+
for (const { definition, entity, type } of triggersDef) {
|
|
41
|
+
const name = definition.name
|
|
42
|
+
const target = this.resolveTarget(name, definition, targetsRegistry)
|
|
43
|
+
let trigger: Actions.AnyTrigger
|
|
44
|
+
if (type === 'basic') {
|
|
45
|
+
trigger = {
|
|
46
|
+
target,
|
|
47
|
+
...this.createBasicTrigger(name, entity.name, definition),
|
|
48
|
+
}
|
|
49
|
+
} else if (type === 'watch') {
|
|
50
|
+
trigger = {
|
|
51
|
+
target,
|
|
52
|
+
...this.createWatchTrigger(name, entity.name, definition),
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
assertNever(type)
|
|
56
|
+
}
|
|
57
|
+
triggers[name] = trigger
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
return { triggers, targets: targetsRegistry.getTargets() }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private resolveTarget(triggerName: string, targetDefinition: TargetDefinition, targetRegistry: ActionsTargetRegistry): string {
|
|
65
|
+
const generatedName = `${triggerName}_target`
|
|
66
|
+
if ('target' in targetDefinition) {
|
|
67
|
+
if (targetDefinition.target instanceof ActionsTarget) {
|
|
68
|
+
const registeredName = targetRegistry.getName(targetDefinition.target.definition)
|
|
69
|
+
if (!registeredName) {
|
|
70
|
+
throw `Target of ${triggerName} trigger is not registered. Have you exported it?`
|
|
71
|
+
}
|
|
72
|
+
return registeredName
|
|
73
|
+
} else {
|
|
74
|
+
const { name, ...def } = targetDefinition.target
|
|
75
|
+
const resolvedName = name ?? generatedName
|
|
76
|
+
targetRegistry.register(resolvedName, { name: generatedName, ...def })
|
|
77
|
+
return resolvedName
|
|
78
|
+
}
|
|
79
|
+
} else if ('webhook' in targetDefinition) {
|
|
80
|
+
if (typeof targetDefinition.webhook === 'string') {
|
|
81
|
+
targetRegistry.register(generatedName, {
|
|
82
|
+
name: generatedName,
|
|
83
|
+
type: 'webhook',
|
|
84
|
+
url: targetDefinition.webhook,
|
|
85
|
+
})
|
|
86
|
+
} else {
|
|
87
|
+
targetRegistry.register(generatedName, {
|
|
88
|
+
type: 'webhook',
|
|
89
|
+
...targetDefinition.webhook,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
return generatedName
|
|
93
|
+
}
|
|
94
|
+
return assertNever(targetDefinition)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private createWatchTrigger(name: string, entity: string, trigger: WatchTriggerDefinition): Omit<Actions.WatchTrigger, 'target'> {
|
|
98
|
+
return {
|
|
99
|
+
type: 'watch',
|
|
100
|
+
entity,
|
|
101
|
+
name,
|
|
102
|
+
watch: this.parseSelection(trigger.watch),
|
|
103
|
+
selection: trigger.selection ? this.parseSelection(trigger.selection) : undefined,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private createBasicTrigger(name: string, entity: string, trigger: BasicTriggerDefinition): Omit<Actions.BasicTrigger, 'target'> {
|
|
108
|
+
return {
|
|
109
|
+
type: 'basic',
|
|
110
|
+
entity,
|
|
111
|
+
name,
|
|
112
|
+
create: trigger.create ?? false,
|
|
113
|
+
delete: trigger.delete ?? false,
|
|
114
|
+
update: trigger.update ?? false,
|
|
115
|
+
selection: trigger.selection ? this.parseSelection(trigger.selection) : undefined,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private parseSelection(selection: Actions.SelectionNode | string): Actions.SelectionNode {
|
|
120
|
+
if (typeof selection !== 'string') {
|
|
121
|
+
return selection
|
|
122
|
+
}
|
|
123
|
+
const result = parse(`{
|
|
124
|
+
${selection}
|
|
125
|
+
}`)
|
|
126
|
+
for (const def of result.definitions) {
|
|
127
|
+
if (def.kind === 'OperationDefinition') {
|
|
128
|
+
return this.processSelectionSet(def.selectionSet.selections)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw new Error()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private processSelectionSet(selectionSet: readonly SelectionNode[]): Actions.SelectionNode {
|
|
135
|
+
return selectionSet.map(it => {
|
|
136
|
+
if (it.kind !== 'Field') {
|
|
137
|
+
throw `${it.kind} is not supported.`
|
|
138
|
+
}
|
|
139
|
+
if (it.arguments?.length === 0 && it.selectionSet === undefined) {
|
|
140
|
+
return it.name.value
|
|
141
|
+
}
|
|
142
|
+
const args = Object.fromEntries(it.arguments?.map(it => [it.name, parseArguments(it.value)]) ?? [])
|
|
143
|
+
return [it.name.value, args, it.selectionSet ? this.processSelectionSet(it.selectionSet.selections) : []]
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
class ActionsTargetRegistry {
|
|
149
|
+
private targets: Record<string, Actions.AnyTarget> = {}
|
|
150
|
+
private targetsInverseMap = new Map<Omit<Actions.AnyTarget, 'name'>, string>()
|
|
151
|
+
|
|
152
|
+
public register(name: string, target: Omit<Actions.AnyTarget, 'name'> & { name?: string }): void {
|
|
153
|
+
if (this.targets[name]) {
|
|
154
|
+
throw `Duplicate trigger target name ${name}`
|
|
155
|
+
}
|
|
156
|
+
this.targets[name] = { name, ...target }
|
|
157
|
+
this.targetsInverseMap.set(target, name)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public getName(target: Omit<Actions.AnyTarget, 'name'>): string | undefined {
|
|
161
|
+
return this.targetsInverseMap.get(target)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public getTargets() {
|
|
165
|
+
return this.targets
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const parseArguments = (ast: ValueNode): any => {
|
|
170
|
+
switch (ast.kind) {
|
|
171
|
+
case Kind.STRING:
|
|
172
|
+
return ast.value
|
|
173
|
+
case Kind.BOOLEAN:
|
|
174
|
+
return ast.value
|
|
175
|
+
case Kind.FLOAT:
|
|
176
|
+
return parseFloat(ast.value)
|
|
177
|
+
case Kind.INT:
|
|
178
|
+
return Number(ast.value)
|
|
179
|
+
case Kind.LIST:
|
|
180
|
+
return ast.values.map(it => parseArguments(it))
|
|
181
|
+
case Kind.OBJECT:
|
|
182
|
+
return Object.fromEntries(ast.fields.map(it => [it.name.value, parseArguments(it.value)]))
|
|
183
|
+
case Kind.ENUM:
|
|
184
|
+
return ast.value
|
|
185
|
+
case Kind.NULL:
|
|
186
|
+
return null
|
|
187
|
+
case Kind.VARIABLE:
|
|
188
|
+
throw `Variables are not supported here.`
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createMetadataStore } from '../../../utils'
|
|
2
|
+
import { BasicTriggerDefinition, WatchTriggerDefinition } from '../triggers'
|
|
3
|
+
|
|
4
|
+
type StoredTrigger =
|
|
5
|
+
| { type: 'basic'; definition: BasicTriggerDefinition }
|
|
6
|
+
| { type: 'watch'; definition: WatchTriggerDefinition }
|
|
7
|
+
|
|
8
|
+
export const triggersStore = createMetadataStore<StoredTrigger[]>([])
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Actions } from '@contember/schema'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export class ActionsTarget {
|
|
5
|
+
constructor(
|
|
6
|
+
public readonly name: string | undefined,
|
|
7
|
+
public readonly definition: Omit<Actions.AnyTarget, 'name'>,
|
|
8
|
+
) {
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const createTarget = ({
|
|
13
|
+
name,
|
|
14
|
+
...definition
|
|
15
|
+
}: Actions.AnyTarget & { name?: string }) => new ActionsTarget(name, definition)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { DecoratorFunction, EntityConstructor } from '../../utils'
|
|
2
|
+
import { Actions } from '@contember/schema'
|
|
3
|
+
import { triggersStore } from './internal/store'
|
|
4
|
+
import { ActionsTarget } from './targets'
|
|
5
|
+
|
|
6
|
+
export type TriggerWebhookDefinition = {
|
|
7
|
+
webhook:
|
|
8
|
+
| string
|
|
9
|
+
| Omit<Actions.WebhookTarget, 'type'>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type TriggerTargetDefinition = {
|
|
13
|
+
target: (Omit<Actions.AnyTarget, 'name'> & { name?: string }) | ActionsTarget
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type TargetDefinition =
|
|
17
|
+
| TriggerWebhookDefinition
|
|
18
|
+
| TriggerTargetDefinition
|
|
19
|
+
|
|
20
|
+
export type BasicTriggerDefinition =
|
|
21
|
+
& {
|
|
22
|
+
readonly name: string
|
|
23
|
+
readonly create?: boolean
|
|
24
|
+
readonly delete?: boolean
|
|
25
|
+
readonly update?: boolean | readonly string[]
|
|
26
|
+
readonly selection?: Actions.SelectionNode | string
|
|
27
|
+
}
|
|
28
|
+
& TargetDefinition
|
|
29
|
+
|
|
30
|
+
export type WatchTriggerDefinition =
|
|
31
|
+
& {
|
|
32
|
+
readonly name: string
|
|
33
|
+
readonly watch: Actions.SelectionNode | string
|
|
34
|
+
readonly selection?: Actions.SelectionNode | string
|
|
35
|
+
}
|
|
36
|
+
& TargetDefinition
|
|
37
|
+
|
|
38
|
+
export const trigger = <T>(definition: BasicTriggerDefinition): DecoratorFunction<T> => (entity: EntityConstructor<T>) => {
|
|
39
|
+
triggersStore.update(entity, it => [...it, { type: 'basic', definition }])
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const watch = <T>(definition: WatchTriggerDefinition): DecoratorFunction<T> => (entity: EntityConstructor<T>) => {
|
|
43
|
+
triggersStore.update(entity, it => [...it, { type: 'watch', definition }])
|
|
44
|
+
}
|
|
45
|
+
|
package/src/createSchema.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SchemaDefinition } from './model'
|
|
2
2
|
import * as InputValidation from './validation'
|
|
3
3
|
import * as AclDefinition from './acl/definition'
|
|
4
|
+
import * as ActionsDefinition from './actions/definition'
|
|
4
5
|
import { Schema } from '@contember/schema'
|
|
5
6
|
import { emptySchema } from '@contember/schema-utils'
|
|
6
7
|
|
|
@@ -8,6 +9,7 @@ export const createSchema = (definitions: Record<string, any>, modifyCallback?:
|
|
|
8
9
|
const model = SchemaDefinition.createModel(definitions)
|
|
9
10
|
const validation = InputValidation.parseDefinition(definitions)
|
|
10
11
|
const acl = AclDefinition.createAcl(definitions, model)
|
|
11
|
-
const
|
|
12
|
+
const actions = ActionsDefinition.createActions(definitions)
|
|
13
|
+
const schema = { ...emptySchema, model, validation, acl, actions }
|
|
12
14
|
return modifyCallback ? modifyCallback(schema) : schema
|
|
13
15
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,5 +2,6 @@ export { AllowAllPermissionFactory } from '@contember/schema-utils'
|
|
|
2
2
|
export { PermissionsBuilder } from './acl/builder/PermissionsBuilder'
|
|
3
3
|
export * as InputValidation from './validation'
|
|
4
4
|
export * as AclDefinition from './acl/definition'
|
|
5
|
+
export * as ActionsDefinition from './actions/definition'
|
|
5
6
|
export * from './createSchema'
|
|
6
7
|
export * from './model'
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { SchemaDefinition as def, ActionsDefinition as actions } from '../../src'
|
|
2
|
+
import { Model } from '@contember/schema'
|
|
3
|
+
import { expect, test } from 'vitest'
|
|
4
|
+
import { createActions } from '../../src/actions/definition'
|
|
5
|
+
|
|
6
|
+
namespace SimpleActions {
|
|
7
|
+
|
|
8
|
+
@actions.watch({
|
|
9
|
+
name: 'book_watch',
|
|
10
|
+
watch: `
|
|
11
|
+
title
|
|
12
|
+
tags {
|
|
13
|
+
name
|
|
14
|
+
}
|
|
15
|
+
`,
|
|
16
|
+
webhook: '%webhookUrl%/book/updated',
|
|
17
|
+
})
|
|
18
|
+
export class Book {
|
|
19
|
+
title = def.stringColumn()
|
|
20
|
+
tags = def.manyHasMany(Tag)
|
|
21
|
+
category = def.manyHasOne(Category)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class Tag {
|
|
25
|
+
locales = def.oneHasMany(TagLocale, 'tag')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@def.Unique('tag', 'locale')
|
|
29
|
+
export class TagLocale {
|
|
30
|
+
tag = def.manyHasOne(Tag, 'locales')
|
|
31
|
+
name = def.stringColumn()
|
|
32
|
+
locale = def.manyHasOne(Locale)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
export class Category {
|
|
37
|
+
locales = def.oneHasMany(CategoryLocale, 'category')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@def.Unique('category', 'locale')
|
|
41
|
+
export class CategoryLocale {
|
|
42
|
+
category = def.manyHasOne(Category, 'locales')
|
|
43
|
+
name = def.stringColumn()
|
|
44
|
+
locale = def.manyHasOne(Locale)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class Locale {
|
|
48
|
+
code = def.stringColumn().notNull().unique()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test('read actions schema', () => {
|
|
53
|
+
expect(createActions(SimpleActions)).toMatchInlineSnapshot(`
|
|
54
|
+
{
|
|
55
|
+
"targets": {
|
|
56
|
+
"book_watch_target": {
|
|
57
|
+
"name": "book_watch_target",
|
|
58
|
+
"type": "webhook",
|
|
59
|
+
"url": "%webhookUrl%/book/updated",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
"triggers": {
|
|
63
|
+
"book_watch": {
|
|
64
|
+
"entity": "Book",
|
|
65
|
+
"name": "book_watch",
|
|
66
|
+
"selection": undefined,
|
|
67
|
+
"target": "book_watch_target",
|
|
68
|
+
"type": "watch",
|
|
69
|
+
"watch": [
|
|
70
|
+
"title",
|
|
71
|
+
[
|
|
72
|
+
"tags",
|
|
73
|
+
{},
|
|
74
|
+
[
|
|
75
|
+
"name",
|
|
76
|
+
],
|
|
77
|
+
],
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
`)
|
|
83
|
+
})
|