@xeplr/workflow-engine 0.0.0-stage → 1.0.1

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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +182 -2
  3. package/db/xcfgSetup.js +8 -0
  4. package/index.js +290 -0
  5. package/lib/actionCatalog.js +197 -0
  6. package/lib/actions/jobChainSteps.js +79 -0
  7. package/lib/actions/jobRun.js +171 -0
  8. package/lib/actions/screenShow.js +71 -0
  9. package/lib/auth.js +19 -0
  10. package/lib/db.js +82 -0
  11. package/lib/envExposed.js +87 -0
  12. package/lib/flows.js +916 -0
  13. package/lib/jobChains.js +426 -0
  14. package/lib/workflowRunner.js +652 -0
  15. package/migrations/0003_workflows.sql +35 -0
  16. package/migrations/0004_workflow_steps.sql +79 -0
  17. package/migrations/0005_workflow_runs.sql +49 -0
  18. package/migrations/0006_workflow_step_runs.sql +42 -0
  19. package/migrations/0007_workflow_resume_keys.sql +42 -0
  20. package/migrations/0008_workflow_run_edges.sql +43 -0
  21. package/migrations/0009_workflow_steps_layout.sql +11 -0
  22. package/migrations/0010_workflow_steps_sample_output.sql +17 -0
  23. package/migrations/0011_workflow_steps_params.sql +27 -0
  24. package/migrations/0012_workflows_kind.sql +27 -0
  25. package/migrations/0013_workflows_key.sql +48 -0
  26. package/migrations/0014_workflows_trigger.sql +30 -0
  27. package/migrations-tenants/0001_companies.sql +23 -0
  28. package/migrations-tenants/0002_workspaces.sql +22 -0
  29. package/models/Company.js +82 -0
  30. package/models/Workflow.js +67 -0
  31. package/models/WorkflowResumeKey.js +32 -0
  32. package/models/WorkflowRun.js +64 -0
  33. package/models/WorkflowRunEdge.js +49 -0
  34. package/models/WorkflowStep.js +66 -0
  35. package/models/WorkflowStepRun.js +49 -0
  36. package/models/Workspace.js +75 -0
  37. package/models/index.js +25 -0
  38. package/package.json +47 -4
@@ -0,0 +1,67 @@
1
+ var { BaseModel } = require('@xeplr/db');
2
+
3
+ // The document. Mounted through genericRoute as the parent of `steps` — one
4
+ // POST /workflows/save writes the workflow and its whole step list in a
5
+ // single transaction (see routes/index.js). Everything about a RUN of this
6
+ // document — WorkflowRun, WorkflowStepRun, WorkflowResumeKey,
7
+ // WorkflowRunEdge — is deliberately a separate model family, not part of
8
+ // this hierarchy: a run is not something you edit by saving the workflow.
9
+
10
+ class Workflow extends BaseModel {
11
+ static get tableName() { return 'workflows'; }
12
+ static get idColumn() { return 'id'; }
13
+
14
+ static get jsonSchema() {
15
+ return {
16
+ type: 'object',
17
+ required: ['id', 'name'],
18
+ properties: {
19
+ id: { type: 'string', maxLength: 25 },
20
+ name: { type: 'string', maxLength: 255 },
21
+ // A STABLE, AUTHOR-CHOSEN HANDLE, and only a FLOW has one. The flows
22
+ // facade addresses a flow by key in its URLs (GET /flows/:key), so the
23
+ // app that designs the screens refers to it by a name it chose rather
24
+ // than by an id this database minted. Null for every ordinary
25
+ // workflow — the builder finds those by id. See migrations/0013.
26
+ key: { type: ['string', 'null'], maxLength: 64 },
27
+ description: { type: ['string', 'null'], maxLength: 1000 },
28
+ // [{ name, type, required, default, description, order }]
29
+ params: { type: ['array', 'null'] },
30
+ // WHAT STARTS A RUN, as the designer recorded it — { kind, … }, one
31
+ // shape per kind (see migrations/0014). The engine does not arm
32
+ // anything from it yet; it is the intent the arming work will read.
33
+ trigger: { type: ['object', 'null'] },
34
+ status: { type: 'string', maxLength: 20 },
35
+ // WHAT THE BUILDER OFFERS, not what the engine does — 'workflow' (the
36
+ // action catalogue), 'jobs' (the job list), or 'screens' (a flow,
37
+ // designed in another app entirely through the /flows facade and
38
+ // read-only in this product's own builder). workflowRunner never reads
39
+ // it. See migrations/0012 and 0013.
40
+ kind: { type: 'string', enum: ['workflow', 'jobs', 'screens', 'job-chain'] },
41
+ isActive: { type: 'boolean' },
42
+ recordCreatedDate: { type: ['string', 'null'] },
43
+ recordModifiedDate: { type: ['string', 'null'] },
44
+ recordCreatedBy: { type: ['string', 'null'], maxLength: 25 },
45
+ recordModifiedBy: { type: ['string', 'null'], maxLength: 25 }
46
+ }
47
+ };
48
+ }
49
+
50
+ $beforeInsert() {
51
+ super.$beforeInsert();
52
+ if (!this.status) this.status = 'draft';
53
+ }
54
+
55
+ static get relationMappings() {
56
+ var WorkflowStep = require('./WorkflowStep');
57
+ return {
58
+ steps: {
59
+ relation: BaseModel.HasManyRelation,
60
+ modelClass: WorkflowStep,
61
+ join: { from: 'workflows.id', to: 'workflow_steps.workflowId' }
62
+ }
63
+ };
64
+ }
65
+ }
66
+
67
+ module.exports = Workflow;
@@ -0,0 +1,32 @@
1
+ var { BaseModel } = require('@xeplr/db');
2
+
3
+ // The entire wait/resume mechanism lives behind this one table — see
4
+ // 0007_workflow_resume_keys.sql for the reasoning. Nothing outside
5
+ // lib/workflowRunner.js should query this model directly; the public surface
6
+ // is workflowRunner.resumeByKey(key, output).
7
+
8
+ class WorkflowResumeKey extends BaseModel {
9
+ static get tableName() { return 'workflow_resume_keys'; }
10
+ static get idColumn() { return 'id'; }
11
+
12
+ static get jsonSchema() {
13
+ return {
14
+ type: 'object',
15
+ required: ['id', 'runId', 'stepId', 'key'],
16
+ properties: {
17
+ id: { type: 'string', maxLength: 25 },
18
+ runId: { type: 'string', maxLength: 25 },
19
+ stepId: { type: 'string', maxLength: 25 },
20
+ key: { type: 'string', maxLength: 64 },
21
+ consumedDate: { type: ['string', 'null'] },
22
+ isActive: { type: 'boolean' },
23
+ recordCreatedDate: { type: ['string', 'null'] },
24
+ recordModifiedDate: { type: ['string', 'null'] },
25
+ recordCreatedBy: { type: ['string', 'null'], maxLength: 25 },
26
+ recordModifiedBy: { type: ['string', 'null'], maxLength: 25 }
27
+ }
28
+ };
29
+ }
30
+ }
31
+
32
+ module.exports = WorkflowResumeKey;
@@ -0,0 +1,64 @@
1
+ var { BaseModel } = require('@xeplr/db');
2
+
3
+ // One occurrence of a Workflow. Not part of the workflow document's
4
+ // genericRoute hierarchy — a run is created and driven by lib/workflowRunner,
5
+ // never hand-edited via a document save. Deliberately independent of
6
+ // @xeplr/jobs' Job/JobOccurrence: a workflow ties multiple steps (and, later,
7
+ // possibly jobs) together, but owns its own execution bookkeeping rather than
8
+ // borrowing theirs.
9
+
10
+ class WorkflowRun extends BaseModel {
11
+ static get tableName() { return 'workflow_runs'; }
12
+ static get idColumn() { return 'id'; }
13
+
14
+ static get jsonSchema() {
15
+ return {
16
+ type: 'object',
17
+ required: ['id', 'workflowId', 'status'],
18
+ properties: {
19
+ id: { type: 'string', maxLength: 25 },
20
+ workflowId: { type: 'string', maxLength: 25 },
21
+ status: { type: 'string', enum: ['queued', 'running', 'waiting', 'success', 'failed'] },
22
+ params: { type: ['object', 'null'] },
23
+ // Set only on a child run spawned by an 'each' transition — the one
24
+ // element this occurrence exists to process. See WorkflowRunEdge.
25
+ item: { type: ['object', 'null'] },
26
+ trigger: { type: ['string', 'null'], maxLength: 64 },
27
+ startedAt: { type: ['string', 'null'] },
28
+ finishedAt: { type: ['string', 'null'] },
29
+ durationMs: { type: ['integer', 'null'] },
30
+ error: { type: ['object', 'null'] },
31
+ isActive: { type: 'boolean' },
32
+ recordCreatedDate: { type: ['string', 'null'] },
33
+ recordModifiedDate: { type: ['string', 'null'] },
34
+ recordCreatedBy: { type: ['string', 'null'], maxLength: 25 },
35
+ recordModifiedBy: { type: ['string', 'null'], maxLength: 25 }
36
+ }
37
+ };
38
+ }
39
+
40
+ $beforeInsert() {
41
+ super.$beforeInsert();
42
+ if (!this.status) this.status = 'queued';
43
+ if (!this.trigger) this.trigger = 'manual';
44
+ }
45
+
46
+ static get relationMappings() {
47
+ var Workflow = require('./Workflow');
48
+ var WorkflowStepRun = require('./WorkflowStepRun');
49
+ return {
50
+ workflow: {
51
+ relation: BaseModel.BelongsToOneRelation,
52
+ modelClass: Workflow,
53
+ join: { from: 'workflow_runs.workflowId', to: 'workflows.id' }
54
+ },
55
+ stepRuns: {
56
+ relation: BaseModel.HasManyRelation,
57
+ modelClass: WorkflowStepRun,
58
+ join: { from: 'workflow_runs.id', to: 'workflow_step_runs.runId' }
59
+ }
60
+ };
61
+ }
62
+ }
63
+
64
+ module.exports = WorkflowRun;
@@ -0,0 +1,49 @@
1
+ var { BaseModel } = require('@xeplr/db');
2
+
3
+ // The only place a run's family tree lives — see 0008_workflow_run_edges.sql.
4
+ // workflow_steps stays flat; fan-out ancestry is entirely a run-time fact
5
+ // recorded here, never something an authored step definition carries.
6
+
7
+ class WorkflowRunEdge extends BaseModel {
8
+ static get tableName() { return 'workflow_run_edges'; }
9
+ static get idColumn() { return 'id'; }
10
+
11
+ static get jsonSchema() {
12
+ return {
13
+ type: 'object',
14
+ required: ['id', 'parentRunId', 'childRunId'],
15
+ properties: {
16
+ id: { type: 'string', maxLength: 25 },
17
+ parentRunId: { type: 'string', maxLength: 25 },
18
+ childRunId: { type: 'string', maxLength: 25 },
19
+ sourceStepKey: { type: ['string', 'null'], maxLength: 64 },
20
+ itemIndex: { type: ['integer', 'null'] },
21
+ itemKey: { type: ['string', 'null'], maxLength: 255 },
22
+ item: { type: ['object', 'null'] },
23
+ isActive: { type: 'boolean' },
24
+ recordCreatedDate: { type: ['string', 'null'] },
25
+ recordModifiedDate: { type: ['string', 'null'] },
26
+ recordCreatedBy: { type: ['string', 'null'], maxLength: 25 },
27
+ recordModifiedBy: { type: ['string', 'null'], maxLength: 25 }
28
+ }
29
+ };
30
+ }
31
+
32
+ static get relationMappings() {
33
+ var WorkflowRun = require('./WorkflowRun');
34
+ return {
35
+ parentRun: {
36
+ relation: BaseModel.BelongsToOneRelation,
37
+ modelClass: WorkflowRun,
38
+ join: { from: 'workflow_run_edges.parentRunId', to: 'workflow_runs.id' }
39
+ },
40
+ childRun: {
41
+ relation: BaseModel.BelongsToOneRelation,
42
+ modelClass: WorkflowRun,
43
+ join: { from: 'workflow_run_edges.childRunId', to: 'workflow_runs.id' }
44
+ }
45
+ };
46
+ }
47
+ }
48
+
49
+ module.exports = WorkflowRunEdge;
@@ -0,0 +1,66 @@
1
+ var { BaseModel } = require('@xeplr/db');
2
+
3
+ class WorkflowStep extends BaseModel {
4
+ static get tableName() { return 'workflow_steps'; }
5
+ static get idColumn() { return 'id'; }
6
+
7
+ static get jsonSchema() {
8
+ return {
9
+ type: 'object',
10
+ required: ['id', 'workflowId', 'stepKey', 'actionName'],
11
+ properties: {
12
+ id: { type: 'string', maxLength: 25 },
13
+ workflowId: { type: 'string', maxLength: 25 },
14
+ stepKey: { type: 'string', maxLength: 64 },
15
+ name: { type: ['string', 'null'], maxLength: 255 },
16
+ actionName: { type: 'string', maxLength: 128 },
17
+ values: { type: ['object', 'null'] },
18
+ kind: { type: 'string', enum: ['auto', 'wait'] },
19
+ timeoutMs: { type: ['integer', 'null'] },
20
+ onError: { type: 'string', enum: ['stop', 'continue'] },
21
+ // [{ condition: expressionHandlerExpr|null, mode: 'single'|'each', target: stepKey|'end_success'|'end_failed' }]
22
+ transitions: { type: ['array', 'null'] },
23
+ joinStep: { type: ['string', 'null'], maxLength: 64 },
24
+ position: { type: 'integer' },
25
+ // { x, y } canvas pixels — where the builder draws this step. Purely
26
+ // visual; never read by workflowRunner. See migrations/0009.
27
+ layout: { type: ['object', 'null'] },
28
+ // An EXAMPLE of what this step returns, for the builder's field
29
+ // picker — never what it did return (that is on workflow_step_runs),
30
+ // and never read by workflowRunner. See migrations/0010.
31
+ sampleOutput: { type: ['object', 'null'] },
32
+ // What this step needs the CALLER to supply at run start:
33
+ // [{ name, type, required, default, description, order, sample? }] —
34
+ // the same field shape as an action's inputSchema. UNLIKE layout and
35
+ // sampleOutput, workflowRunner DOES read this: every step's
36
+ // declaration unions into the schema a run is validated against
37
+ // before any step executes. See migrations/0011.
38
+ params: { type: ['array', 'null'] },
39
+ isActive: { type: 'boolean' },
40
+ recordCreatedDate: { type: ['string', 'null'] },
41
+ recordModifiedDate: { type: ['string', 'null'] },
42
+ recordCreatedBy: { type: ['string', 'null'], maxLength: 25 },
43
+ recordModifiedBy: { type: ['string', 'null'], maxLength: 25 }
44
+ }
45
+ };
46
+ }
47
+
48
+ $beforeInsert() {
49
+ super.$beforeInsert();
50
+ if (!this.kind) this.kind = 'auto';
51
+ if (!this.onError) this.onError = 'stop';
52
+ }
53
+
54
+ static get relationMappings() {
55
+ var Workflow = require('./Workflow');
56
+ return {
57
+ workflow: {
58
+ relation: BaseModel.BelongsToOneRelation,
59
+ modelClass: Workflow,
60
+ join: { from: 'workflow_steps.workflowId', to: 'workflows.id' }
61
+ }
62
+ };
63
+ }
64
+ }
65
+
66
+ module.exports = WorkflowStep;
@@ -0,0 +1,49 @@
1
+ var { BaseModel } = require('@xeplr/db');
2
+
3
+ class WorkflowStepRun extends BaseModel {
4
+ static get tableName() { return 'workflow_step_runs'; }
5
+ static get idColumn() { return 'id'; }
6
+
7
+ static get jsonSchema() {
8
+ return {
9
+ type: 'object',
10
+ required: ['id', 'runId', 'status'],
11
+ properties: {
12
+ id: { type: 'string', maxLength: 25 },
13
+ runId: { type: 'string', maxLength: 25 },
14
+ stepId: { type: ['string', 'null'], maxLength: 25 },
15
+ stepKey: { type: ['string', 'null'], maxLength: 64 },
16
+ actionName: { type: ['string', 'null'], maxLength: 128 },
17
+ status: { type: 'string', enum: ['pending', 'running', 'waiting', 'success', 'failed', 'skipped'] },
18
+ input: { type: ['object', 'null'] },
19
+ output: { type: ['object', 'null'] },
20
+ error: { type: ['object', 'null'] },
21
+ durationMs: { type: ['integer', 'null'] },
22
+ position: { type: 'integer' },
23
+ isActive: { type: 'boolean' },
24
+ recordCreatedDate: { type: ['string', 'null'] },
25
+ recordModifiedDate: { type: ['string', 'null'] },
26
+ recordCreatedBy: { type: ['string', 'null'], maxLength: 25 },
27
+ recordModifiedBy: { type: ['string', 'null'], maxLength: 25 }
28
+ }
29
+ };
30
+ }
31
+
32
+ $beforeInsert() {
33
+ super.$beforeInsert();
34
+ if (!this.status) this.status = 'pending';
35
+ }
36
+
37
+ static get relationMappings() {
38
+ var WorkflowRun = require('./WorkflowRun');
39
+ return {
40
+ run: {
41
+ relation: BaseModel.BelongsToOneRelation,
42
+ modelClass: WorkflowRun,
43
+ join: { from: 'workflow_step_runs.runId', to: 'workflow_runs.id' }
44
+ }
45
+ };
46
+ }
47
+ }
48
+
49
+ module.exports = WorkflowStepRun;
@@ -0,0 +1,75 @@
1
+ // Workspace — the second tenancy level, under a company. Plain CRUD record;
2
+ // siloed access is granted per workspace in the auth DB. companyId is required
3
+ // (a workspace always belongs to a company).
4
+ //
5
+ // Single MT level (mtLevels: 1) — a workspace row is scoped by its company
6
+ // (mtId1) only. It doesn't have a meaningful mtId2 of its own (a workspace
7
+ // isn't scoped "by workspace"); resources that live INSIDE a workspace
8
+ // (workflows, runs) stay at the app's full registered level count (2: company
9
+ // + workspace) via the default.
10
+ var { BaseModel } = require('@xeplr/db');
11
+ var { generateId } = require('@xeplr/utils/lib/helpers');
12
+
13
+ // Cross-DB — see Company.js for the identical pattern this mirrors: a
14
+ // CompanyAdmin's l1 grant does NOT imply l2 access (mtMembershipMiddleware
15
+ // checks each configured level independently), so without this, entering a
16
+ // just-created workspace 403s with "Not authorized for workspaceId ..." exactly
17
+ // like an ungranted company did before Company.$afterInsert existed.
18
+ function auth() { return require('../lib/auth'); }
19
+
20
+ async function grantWorkspaceCreator(userId, workspaceId) {
21
+ var a = auth();
22
+ await a.ready();
23
+ var Role = a.model('Role');
24
+ var UserTenantsMapping = a.model('UserTenantsMapping');
25
+
26
+ var role = await Role.query().where({ name: 'Creator' }).first();
27
+ if (!role) return; // not seeded (yet) — skip rather than throw
28
+
29
+ var existing = await UserTenantsMapping.query()
30
+ .where({ userId: userId, level: 'l2', value: workspaceId }).first();
31
+ if (existing) return;
32
+
33
+ await UserTenantsMapping.query().insert({
34
+ id: generateId(),
35
+ userId: userId,
36
+ level: 'l2',
37
+ value: workspaceId,
38
+ roleId: role.id,
39
+ isActive: true
40
+ });
41
+ }
42
+
43
+ class Workspace extends BaseModel {
44
+ static get tableName() { return 'workspaces'; }
45
+ static get idColumn() { return 'id'; }
46
+ static get mtLevels() { return 1; }
47
+
48
+ static get jsonAttributes() { return ['tags']; }
49
+
50
+ static get jsonSchema() {
51
+ return BaseModel.schema({
52
+ required: ['companyId', 'name'],
53
+ properties: { tags: { type: ['array', 'null'] } }
54
+ });
55
+ }
56
+
57
+ // Deferred via queryContext.afterCommit — see Company.js's $afterInsert for
58
+ // why this can't grant inline.
59
+ async $afterInsert(queryContext) {
60
+ await super.$afterInsert(queryContext);
61
+ if (queryContext && queryContext.user && queryContext.user.id && Array.isArray(queryContext.afterCommit)) {
62
+ var userId = queryContext.user.id;
63
+ var workspaceId = this.id;
64
+ queryContext.afterCommit.push(async function() {
65
+ try {
66
+ await grantWorkspaceCreator(userId, workspaceId);
67
+ } catch (err) {
68
+ console.error('[Workspace] failed to grant Creator access to creator:', err.message);
69
+ }
70
+ });
71
+ }
72
+ }
73
+ }
74
+
75
+ module.exports = Workspace;
@@ -0,0 +1,25 @@
1
+ // The domain models. Company and Workspace are the tenancy skeleton every
2
+ // xeplr app starts from. Workflow/WorkflowStep are the document, mounted as a
3
+ // genericRoute hierarchy in routes/index.js. WorkflowRun/WorkflowStepRun/
4
+ // WorkflowResumeKey/WorkflowRunEdge are a separate family — an occurrence of
5
+ // a workflow, driven by lib/workflowRunner, never hand-edited via a document
6
+ // save.
7
+ var Company = require('./Company');
8
+ var Workspace = require('./Workspace');
9
+ var Workflow = require('./Workflow');
10
+ var WorkflowStep = require('./WorkflowStep');
11
+ var WorkflowRun = require('./WorkflowRun');
12
+ var WorkflowStepRun = require('./WorkflowStepRun');
13
+ var WorkflowResumeKey = require('./WorkflowResumeKey');
14
+ var WorkflowRunEdge = require('./WorkflowRunEdge');
15
+
16
+ module.exports = {
17
+ Company: Company,
18
+ Workspace: Workspace,
19
+ Workflow: Workflow,
20
+ WorkflowStep: WorkflowStep,
21
+ WorkflowRun: WorkflowRun,
22
+ WorkflowStepRun: WorkflowStepRun,
23
+ WorkflowResumeKey: WorkflowResumeKey,
24
+ WorkflowRunEdge: WorkflowRunEdge
25
+ };
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@xeplr/workflow-engine",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "license": "MIT",
5
+ "description": "The workflow engine — workflow's own database and tables, the action catalog, starting and resuming runs, and the flows facade. No HTTP: @xeplr/workflow-api puts the routes on top.",
6
+ "main": "index.js",
7
+ "files": [
8
+ "index.js",
9
+ "lib/",
10
+ "models/",
11
+ "migrations/",
12
+ "migrations-tenants/",
13
+ "db/",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "test": "node test/run.mjs"
18
+ },
19
+ "author": "xeplr",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/Xeplr/xeplr-workflow-engine.git"
23
+ },
24
+ "peerDependencies": {
25
+ "@xeplr/actions": "^1.1.0",
26
+ "@xeplr/auth": "^1.0.0",
27
+ "@xeplr/db": "^1.0.0",
28
+ "@xeplr/expression-handler": "^2.0.0",
29
+ "@xeplr/schema-handler": "^1.0.0",
30
+ "@xeplr/utils": "^1.0.0"
31
+ },
32
+ "xeplr": {
33
+ "readsFromDisk": [
34
+ "migrations",
35
+ "migrations-tenants"
36
+ ]
37
+ },
38
+ "homepage": "https://github.com/Xeplr/xeplr-workflow-engine#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/Xeplr/xeplr-workflow-engine/issues"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "devDependencies": {
46
+ "@xeplr/jobs": "^2.3.0",
47
+ "knex": "^3.3.0"
48
+ }
49
+ }