@xeplr/workflow 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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +352 -0
  3. package/bin/www +7 -0
  4. package/db/xcfgSetup.js +8 -0
  5. package/env.required.js +41 -0
  6. package/index.js +313 -0
  7. package/lib/actionCatalog.js +193 -0
  8. package/lib/actions/jobRun.js +149 -0
  9. package/lib/actions/screenShow.js +55 -0
  10. package/lib/db.js +82 -0
  11. package/lib/envExposed.js +87 -0
  12. package/lib/flows.js +832 -0
  13. package/lib/flowsRouter.js +113 -0
  14. package/lib/router.js +260 -0
  15. package/lib/workflowRunner.js +649 -0
  16. package/migrations/0001_companies.sql +23 -0
  17. package/migrations/0002_workspaces.sql +22 -0
  18. package/migrations/0003_workflows.sql +35 -0
  19. package/migrations/0004_workflow_steps.sql +79 -0
  20. package/migrations/0005_workflow_runs.sql +49 -0
  21. package/migrations/0006_workflow_step_runs.sql +42 -0
  22. package/migrations/0007_workflow_resume_keys.sql +42 -0
  23. package/migrations/0008_workflow_run_edges.sql +43 -0
  24. package/migrations/0009_workflow_steps_layout.sql +11 -0
  25. package/migrations/0010_workflow_steps_sample_output.sql +17 -0
  26. package/migrations/0011_workflow_steps_params.sql +27 -0
  27. package/migrations/0012_workflows_kind.sql +27 -0
  28. package/migrations/0013_workflows_key.sql +48 -0
  29. package/migrations-auth/0001_workflow_access.sql +129 -0
  30. package/migrations-auth/0003_nav_menus.sql +62 -0
  31. package/migrations-auth/0004_flows_access.sql +76 -0
  32. package/models/Company.js +82 -0
  33. package/models/Workflow.js +63 -0
  34. package/models/WorkflowResumeKey.js +32 -0
  35. package/models/WorkflowRun.js +64 -0
  36. package/models/WorkflowRunEdge.js +49 -0
  37. package/models/WorkflowStep.js +66 -0
  38. package/models/WorkflowStepRun.js +49 -0
  39. package/models/Workspace.js +75 -0
  40. package/models/index.js +25 -0
  41. package/orchestration/standalone.js +123 -0
  42. package/package.json +69 -0
@@ -0,0 +1,76 @@
1
+ -- 0004_flows_access.sql
2
+ -- The `apis` rows for the FLOWS FACADE — the routes an app that designs
3
+ -- screens talks to (lib/flowsRouter.js, mounted at <mount>/flows).
4
+ --
5
+ -- A SEPARATE MIGRATION rather than an edit to 0001, for the reason 0003 gives:
6
+ -- the migrator records what it has applied by filename and will not re-run a
7
+ -- file whose contents changed, so an edit to 0001 would seed nothing at all.
8
+ --
9
+ -- ── which groups these land in, and why ─────────────────────────────────
10
+ --
11
+ -- The same three the workflow routes already use, because a flow is a
12
+ -- workflow and the questions are the same ones:
13
+ --
14
+ -- workflows:view listing flows, reading one, reading a run
15
+ -- workflows:create creating, replacing the steps of a draft, publishing
16
+ -- workflows:run starting a run, and submitting a screen
17
+ --
18
+ -- SUBMITTING IS A RUN RIGHT, NOT A VIEW RIGHT, and that is the only
19
+ -- non-obvious one. It reads like filling in a form, but it moves a run to its
20
+ -- next step and every side effect the rest of the flow has follows from it.
21
+ -- 0001 put running under workflows:run precisely because a run has side
22
+ -- effects, and a Viewer who can submit a screen can cause all of them.
23
+ --
24
+ -- NO MENU ROWS. There is no page in this product for any of this — the
25
+ -- screens live in another app entirely, and this product's own builder shows a
26
+ -- flow read-only through the workflow pages it already has menus for. A menu
27
+ -- row for a page that does not exist puts a dead item in everyone's rail.
28
+ --
29
+ -- Every insert is insert-if-absent, safe to re-run.
30
+
31
+ INSERT INTO "apis" (id, name, "apiGroup", "isPublic", "isActive", "mtId1", "recordCreatedDate", "recordModifiedDate")
32
+ SELECT encode(gen_random_bytes(12), 'hex'), name, "group", false, true, '*', now(), now()
33
+ FROM (VALUES
34
+ ('List flows', 'workflows:view'),
35
+ ('Get flow', 'workflows:view'),
36
+ ('Get flow run', 'workflows:view'),
37
+ ('List flow runs', 'workflows:view'),
38
+ ('Create flow', 'workflows:create'),
39
+ ('Replace flow steps', 'workflows:create'),
40
+ ('Publish flow', 'workflows:create'),
41
+ ('Start flow run', 'workflows:run'),
42
+ ('Submit flow screen', 'workflows:run')
43
+ ) AS v(name, "group")
44
+ WHERE NOT EXISTS (SELECT 1 FROM "apis" a WHERE a."apiGroup" = v."group" AND a.name = v.name);
45
+
46
+ -- Super Admin → everything, including whatever was just added.
47
+ INSERT INTO "apisRolesMapping" (id, "roleId", "apiId", "isActive", "mtId1", "recordCreatedDate", "recordModifiedDate")
48
+ SELECT encode(gen_random_bytes(12), 'hex'), r.id, a.id, true, '*', now(), now()
49
+ FROM "roles" r CROSS JOIN "apis" a
50
+ WHERE r.name = 'Super Admin'
51
+ AND NOT EXISTS (SELECT 1 FROM "apisRolesMapping" m WHERE m."roleId" = r.id AND m."apiId" = a.id);
52
+
53
+ -- The same matrix 0001 established, re-run so it covers the new rows: its own
54
+ -- inserts only ever saw the apis that existed when it ran.
55
+ INSERT INTO "apisRolesMapping" (id, "roleId", "apiId", "isActive", "mtId1", "recordCreatedDate", "recordModifiedDate")
56
+ SELECT encode(gen_random_bytes(12), 'hex'), r.id, a.id, true, '*', now(), now()
57
+ FROM "roles" r CROSS JOIN "apis" a
58
+ WHERE r.name = 'CompanyAdmin'
59
+ AND (a."apiGroup" LIKE 'workflows:%' OR a."apiGroup" LIKE 'configuration:%')
60
+ AND NOT EXISTS (SELECT 1 FROM "apisRolesMapping" m WHERE m."roleId" = r.id AND m."apiId" = a.id);
61
+
62
+ INSERT INTO "apisRolesMapping" (id, "roleId", "apiId", "isActive", "mtId1", "recordCreatedDate", "recordModifiedDate")
63
+ SELECT encode(gen_random_bytes(12), 'hex'), r.id, a.id, true, '*', now(), now()
64
+ FROM "roles" r CROSS JOIN "apis" a
65
+ WHERE r.name = 'Creator'
66
+ AND a."apiGroup" IN ('workflows:view', 'workflows:create', 'workflows:run')
67
+ AND NOT EXISTS (SELECT 1 FROM "apisRolesMapping" m WHERE m."roleId" = r.id AND m."apiId" = a.id);
68
+
69
+ -- Viewer: workflows:view only. Sees the flows and their runs; starts nothing
70
+ -- and submits nothing.
71
+ INSERT INTO "apisRolesMapping" (id, "roleId", "apiId", "isActive", "mtId1", "recordCreatedDate", "recordModifiedDate")
72
+ SELECT encode(gen_random_bytes(12), 'hex'), r.id, a.id, true, '*', now(), now()
73
+ FROM "roles" r CROSS JOIN "apis" a
74
+ WHERE r.name = 'Viewer'
75
+ AND a."apiGroup" = 'workflows:view'
76
+ AND NOT EXISTS (SELECT 1 FROM "apisRolesMapping" m WHERE m."roleId" = r.id AND m."apiId" = a.id);
@@ -0,0 +1,82 @@
1
+ // Company — the top-level tenant (a customer of the workflow product). Plain
2
+ // CRUD record; access is granted per user in the auth DB (userTenantsMapping),
3
+ // not filtered here. When mt is enabled, BaseModel auto-fills mtId from
4
+ // context.
5
+ var { BaseModel } = require('@xeplr/db');
6
+ var { generateId } = require('@xeplr/utils/lib/helpers');
7
+
8
+ // Cross-DB — company lives in this app's own DB, userTenantsMapping/roles live
9
+ // in auth's. Required lazily inside the hook (not at module load) to sidestep
10
+ // any require-order issues between models/ and lib/auth.js at boot.
11
+ function auth() { return require('../lib/auth'); }
12
+
13
+ /**
14
+ * Grants the creator CompanyAdmin on their own new company — otherwise nobody
15
+ * has a userTenantsMapping row for it and every workspace-scoped route 403s
16
+ * immediately (mtMembershipMiddleware has no grant to check against).
17
+ *
18
+ * Best-effort: logs and swallows rather than failing the company creation
19
+ * itself. The company still exists either way, a missing grant is recoverable
20
+ * (an admin can add one), and a rolled-back company creation over a grant
21
+ * hiccup would not be.
22
+ */
23
+ async function grantCompanyAdmin(userId, companyId) {
24
+ var a = auth();
25
+ await a.ready();
26
+ var Role = a.model('Role');
27
+ var UserTenantsMapping = a.model('UserTenantsMapping');
28
+
29
+ var role = await Role.query().where({ name: 'CompanyAdmin' }).first();
30
+ if (!role) return; // not seeded (yet) — skip rather than throw
31
+
32
+ var existing = await UserTenantsMapping.query()
33
+ .where({ userId: userId, level: 'l1', value: companyId }).first();
34
+ if (existing) return;
35
+
36
+ await UserTenantsMapping.query().insert({
37
+ id: generateId(),
38
+ userId: userId,
39
+ level: 'l1',
40
+ value: companyId,
41
+ roleId: role.id,
42
+ isActive: true
43
+ });
44
+ }
45
+
46
+ class Company extends BaseModel {
47
+ static get tableName() { return 'companies'; }
48
+ static get idColumn() { return 'id'; }
49
+ static get multiTenant() { return false; }
50
+
51
+ static get jsonSchema() {
52
+ return BaseModel.schema({ required: ['name'] });
53
+ }
54
+
55
+ // queryContext.user comes from genericController's save() — { user: req.user }
56
+ // set via .context(...) on the insert query. Absent for inserts made outside
57
+ // an authenticated request (e.g. a migration), in which case this no-ops.
58
+ //
59
+ // Deferred via queryContext.afterCommit rather than granted here directly:
60
+ // this hook fires while the Company insert's transaction is still open, but
61
+ // grantCompanyAdmin() writes to the AUTH DB on a separate, non-transactional
62
+ // connection — if a later entry in the same changeset throws and this trx
63
+ // rolls back, an inline grant would already be committed on the other
64
+ // connection, orphaned against a companyId that no longer exists.
65
+ async $afterInsert(queryContext) {
66
+ await super.$afterInsert(queryContext);
67
+ if (!queryContext || !Array.isArray(queryContext.afterCommit)) return;
68
+ if (!queryContext.user || !queryContext.user.id) return;
69
+
70
+ var companyId = this.id;
71
+ var userId = queryContext.user.id;
72
+ queryContext.afterCommit.push(async function() {
73
+ try {
74
+ await grantCompanyAdmin(userId, companyId);
75
+ } catch (err) {
76
+ console.error('[Company] failed to grant CompanyAdmin to creator:', err.message);
77
+ }
78
+ });
79
+ }
80
+ }
81
+
82
+ module.exports = Company;
@@ -0,0 +1,63 @@
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
+ status: { type: 'string', maxLength: 20 },
31
+ // WHAT THE BUILDER OFFERS, not what the engine does — 'workflow' (the
32
+ // action catalogue), 'jobs' (the job list), or 'screens' (a flow,
33
+ // designed in another app entirely through the /flows facade and
34
+ // read-only in this product's own builder). workflowRunner never reads
35
+ // it. See migrations/0012 and 0013.
36
+ kind: { type: 'string', enum: ['workflow', 'jobs', 'screens'] },
37
+ isActive: { type: 'boolean' },
38
+ recordCreatedDate: { type: ['string', 'null'] },
39
+ recordModifiedDate: { type: ['string', 'null'] },
40
+ recordCreatedBy: { type: ['string', 'null'], maxLength: 25 },
41
+ recordModifiedBy: { type: ['string', 'null'], maxLength: 25 }
42
+ }
43
+ };
44
+ }
45
+
46
+ $beforeInsert() {
47
+ super.$beforeInsert();
48
+ if (!this.status) this.status = 'draft';
49
+ }
50
+
51
+ static get relationMappings() {
52
+ var WorkflowStep = require('./WorkflowStep');
53
+ return {
54
+ steps: {
55
+ relation: BaseModel.HasManyRelation,
56
+ modelClass: WorkflowStep,
57
+ join: { from: 'workflows.id', to: 'workflow_steps.workflowId' }
58
+ }
59
+ };
60
+ }
61
+ }
62
+
63
+ 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
+ };