From 6ff5616959fe7721bfae17701e7471c8cbba410d Mon Sep 17 00:00:00 2001 From: Will Ruggiano Date: Tue, 11 Feb 2025 22:11:40 -0800 Subject: [PATCH] fix: primary locations for transitions --- .../__snapshots__/runtime.test.ts.snap | 72 +++++++++++++++++ schema/application/runtime.test.graphql | 25 +++++- schema/system/component/task.ts | 21 +++-- schema/system/component/task_fsm.ts | 79 +++++++++++++++---- sql/add-wtc-unique-constraints.sql | 2 + .../006-primary-location-for-instance.sql | 40 ++++++++++ sql/remove-all-schemas.sql | 1 + .../006-primary-location-for-instance.sql | 9 +++ sql/sqitch.plan | 13 +-- .../006-primary-location-for-instance.sql | 12 +++ 10 files changed, 243 insertions(+), 31 deletions(-) create mode 100644 sql/deploy/006-primary-location-for-instance.sql create mode 100644 sql/revert/006-primary-location-for-instance.sql create mode 100644 sql/verify/006-primary-location-for-instance.sql diff --git a/schema/application/__snapshots__/runtime.test.ts.snap b/schema/application/__snapshots__/runtime.test.ts.snap index a089e9d..204a85e 100644 --- a/schema/application/__snapshots__/runtime.test.ts.snap +++ b/schema/application/__snapshots__/runtime.test.ts.snap @@ -111,6 +111,11 @@ exports[`runtime demo entrypoint query 1`] = ` }, ], }, + "parent": { + "name": { + "value": "Mixing Line", + }, + }, "state": { "__typename": "Open", }, @@ -231,6 +236,11 @@ exports[`runtime demo entrypoint query 1`] = ` }, ], }, + "parent": { + "name": { + "value": "Fill Line", + }, + }, "state": { "__typename": "Open", }, @@ -351,6 +361,11 @@ exports[`runtime demo entrypoint query 1`] = ` }, ], }, + "parent": { + "name": { + "value": "Assembly Line", + }, + }, "state": { "__typename": "Open", }, @@ -471,6 +486,11 @@ exports[`runtime demo entrypoint query 1`] = ` }, ], }, + "parent": { + "name": { + "value": "Cartoning Line", + }, + }, "state": { "__typename": "Open", }, @@ -591,6 +611,11 @@ exports[`runtime demo entrypoint query 1`] = ` }, ], }, + "parent": { + "name": { + "value": "Packaging Line", + }, + }, "state": { "__typename": "Open", }, @@ -632,6 +657,13 @@ exports[`runtime demo start run 1`] = ` "value": "Run", }, }, + "parent": { + "__typename": "Location", + "name": { + "__typename": "Name", + "value": "Mixing Line", + }, + }, "state": { "__typename": "Open", }, @@ -731,6 +763,11 @@ exports[`runtime demo start run 1`] = ` }, ], }, + "parent": { + "name": { + "value": "Mixing Line", + }, + }, "state": { "__typename": "InProgress", "inProgressBy": { @@ -950,6 +987,11 @@ exports[`runtime demo stale: start run (should fail) 1`] = ` }, ], }, + "parent": { + "name": { + "value": "Mixing Line", + }, + }, "state": { "__typename": "InProgress", "inProgressBy": { @@ -1138,6 +1180,11 @@ exports[`runtime demo production -> idle time 1`] = ` }, ], }, + "parent": { + "name": { + "value": "Mixing Line", + }, + }, "state": { "__typename": "InProgress", "inProgressBy": { @@ -1232,6 +1279,11 @@ exports[`runtime demo stale: production -> idle time (should fail) 1`] = ` }, ], }, + "parent": { + "name": { + "value": "Mixing Line", + }, + }, "state": { "__typename": "InProgress", "inProgressBy": { @@ -1347,6 +1399,11 @@ exports[`runtime demo end idle time 1`] = ` }, ], }, + "parent": { + "name": { + "value": "Mixing Line", + }, + }, "state": { "__typename": "InProgress", "inProgressBy": { @@ -1566,6 +1623,11 @@ exports[`runtime demo end idle time 2`] = ` }, ], }, + "parent": { + "name": { + "value": "Mixing Line", + }, + }, "state": { "__typename": "InProgress", "inProgressBy": { @@ -1754,6 +1816,11 @@ exports[`runtime demo production -> downtime 1`] = ` }, ], }, + "parent": { + "name": { + "value": "Mixing Line", + }, + }, "state": { "__typename": "InProgress", "inProgressBy": { @@ -1869,6 +1936,11 @@ exports[`runtime demo end downtime 1`] = ` }, ], }, + "parent": { + "name": { + "value": "Mixing Line", + }, + }, "state": { "__typename": "InProgress", "inProgressBy": { diff --git a/schema/application/runtime.test.graphql b/schema/application/runtime.test.graphql index 7f617a4..fadee36 100644 --- a/schema/application/runtime.test.graphql +++ b/schema/application/runtime.test.graphql @@ -25,6 +25,13 @@ query TestRuntimeEntrypoint( } } hash + parent { + ... on Location { + name { + value + } + } + } state { __typename ... on Closed { @@ -130,6 +137,13 @@ mutation TestRuntimeTransitionMutation( value } } + parent { + ... on Location { + name { + value + } + } + } state { __typename } @@ -172,10 +186,6 @@ fragment Trackables_fragment on Trackable { edges { node { ... on Task { - # chainAgg(overType: ["Runtime", "Idle Time", "Downtime"]) { - # group - # value - # } displayName { name { value @@ -229,6 +239,13 @@ fragment TaskFSM_fragment on TaskStateMachine { } } } + parent { + ... on Location { + name { + value + } + } + } state { __typename ... on InProgress { diff --git a/schema/system/component/task.ts b/schema/system/component/task.ts index 8db350d..d7571aa 100644 --- a/schema/system/component/task.ts +++ b/schema/system/component/task.ts @@ -639,7 +639,7 @@ export async function advanceTask( if (hash !== opts.hash) { console.warn("WARNING: Hash mismatch precludes advancement"); console.debug(`| task: ${t.id}`); - console.debug(`| ours: ${hash}`); + console.debug(`| ours: ${hash}`); console.debug(`| theirs: ${opts.hash}`); return { task: t, @@ -701,11 +701,20 @@ export async function advanceTask( select * from when_open union all select * from when_in_progress + limit 1 `; if (!result) { - assert(false, "seemingly impossible no-op scenario"); - console.warn( + // FIXME: this is possible under concurrency. We are doing OCC here after + // all! What we need to do better here is differentiate between an illegal + // action (e.g. advancing a closed task) and losing the race. If the action + // is legal then the only alternative is that we lost the race, and thus we + // can return a proper diagnostic indicative of this outcome. + // + // TODO: think about how we can remove the assumption inherent in our logic + // here. This would mean allowing for arbitrary task states via something + // like wtnt rather than assuming the canonical open -> in-prog -> closed. + console.error( `Discarding candidate change, presumably because the Task (${t}) is not in a state suitable to advancement.`, ); return { @@ -786,6 +795,9 @@ export async function advanceTask( }; } +/** + * @param mergeAction `replace` overwrites, `keep` does not + */ export function applyAssignments$fragment( ctx: Context, t: Task, @@ -807,15 +819,12 @@ export function applyAssignments$fragment( with cte as ( select i.workinstancecustomerid as _parent, field.workresultinstanceid as _id from public.workinstance as i - inner join public.systag as i_state - on i.workinstancestatusid = i_state.systagid inner join public.workresultinstance as field on i.workinstanceid = field.workresultinstanceworkinstanceid inner join public.workresult as field_t on field.workresultinstanceworkresultid = field_t.workresultid where i.id = ${t._id} - and i_state.systagtype in ('In Progress', 'Complete') and field_t.workresulttypeid = 848 and field_t.workresultentitytypeid = 850 and field_t.workresultisprimary = true diff --git a/schema/system/component/task_fsm.ts b/schema/system/component/task_fsm.ts index 0afad5c..6c092a0 100644 --- a/schema/system/component/task_fsm.ts +++ b/schema/system/component/task_fsm.ts @@ -1,5 +1,4 @@ import { type Sql, type TxSql, sql } from "@/datasources/postgres"; -import { copyFromWorkTemplate } from "@/schema/application/resolvers/Mutation/copyFrom"; import { type Diagnostic, DiagnosticKind } from "@/schema/result"; import type { Mutation } from "@/schema/root"; import type { Context } from "@/schema/types"; @@ -10,7 +9,13 @@ import { match } from "ts-pattern"; import { decodeGlobalId } from ".."; import type { StateMachine } from "../fsm"; import type { Edge } from "../pagination"; -import { type AdvanceTaskOptions, Task, advanceTask } from "./task"; +import { + type AdvanceTaskOptions, + Task, + advanceTask, + applyAssignments$fragment, + applyEdits$fragment, +} from "./task"; export function fsm$fragment(t: Task): Fragment { assert(t._type === "workinstance"); @@ -253,20 +258,64 @@ export async function advanceFsm( } satisfies AdvanceTaskStateMachineResult; }) .with("worktemplate", async () => { - const _ = await copyFromWorkTemplate( - sql, - choice._id, + const result = await sql<[{ instance: ID }]>` + with options as ( + select + w.id as previous_id, + auth.current_identity(w.workinstancecustomerid, ${ctx.auth.userId}) as modified_by, + ( + select l.id + from legacy0.primary_location_for_instance(w.id) as l + ) as location_id + from public.workinstance as w + where w.id = ${decodeGlobalId(fsm.active).id} + ) + + select encode(('workinstance:' || t.instance)::bytea, 'base64') as instance + from + options, + engine0.instantiate( + template_id := ${choice._id}, + location_id := options.location_id, + target_state := 'In Progress', + target_type := 'Task', + modified_by := options.modified_by, + chain_root_id := ${root._id}, + chain_prev_id := options.previous_id + ) as t + group by t.instance; + `; + console.debug(`advance: engine.instantiate.count: ${result.length}`); + + const ins = result.at(0)?.instance; + if (ins) { + const t = new Task({ id: ins }, ctx); + + // In Progress must have a start date. + await sql` + update public.workinstance + set workinstancestartdate = now() + where id = ${t._id} + `; + + // Auto-assign. { - chain: "continue", - previous: decodeGlobalId(fsm.active).id, - // some extra options - autoAssign: true, - carryOverAssignments: true, - fieldOverrides: opts.task.overrides, - withStatus: "inProgress", - }, - ctx, - ); + const ma = "replace"; + const result = await sql`${applyAssignments$fragment(ctx, t, ma)}`; + console.debug( + `advance: applied ${result.count} assignments (mergeAction: ${ma})`, + ); + } + + if (opts.task.overrides) { + const f = applyEdits$fragment(ctx, t, opts.task.overrides); + if (f) { + const result = await sql`${f}`; + console.debug(`advance: applied ${result.count} field-level edits`); + } + } + } + return { root, instantiations: [], diff --git a/sql/add-wtc-unique-constraints.sql b/sql/add-wtc-unique-constraints.sql index 40254e7..7b2860f 100644 --- a/sql/add-wtc-unique-constraints.sql +++ b/sql/add-wtc-unique-constraints.sql @@ -1,6 +1,8 @@ begin ; +drop index temp_wtc_idx; + create unique index temp_wtc_with_result_idx on public.worktemplateconstraint ( worktemplateconstraintcustomerid, worktemplateconstrainttemplateid, diff --git a/sql/deploy/006-primary-location-for-instance.sql b/sql/deploy/006-primary-location-for-instance.sql new file mode 100644 index 0000000..0025924 --- /dev/null +++ b/sql/deploy/006-primary-location-for-instance.sql @@ -0,0 +1,40 @@ +-- Deploy graphql:006-primary-location-for-instance to pg +begin +; + +create function legacy0.primary_location_for_instance(instance_id text) +returns table(id text, _id bigint) +as $$ + with cte as materialized ( + select workresultinstancevalue::bigint as value + from public.workinstance + inner join public.workresult + on workinstanceworktemplateid = workresultworktemplateid + and workresulttypeid = ( + select systagid + from public.systag + where systagparentid = 699 and systagtype = 'Entity' + ) + and workresultentitytypeid = ( + select systagid + from public.systag + where systagparentid = 849 and systagtype = 'Location' + ) + and workresultisprimary = true + inner join public.workresultinstance + on workinstanceid = workresultinstanceworkinstanceid + and workresultid = workresultinstanceworkresultid + where workinstance.id = instance_id + ) + + select locationuuid as id, locationid as _id + from cte, public.location + where cte.value = locationid +$$ +language sql +stable +strict +; + +commit +; diff --git a/sql/remove-all-schemas.sql b/sql/remove-all-schemas.sql index e8c37ae..aff07c5 100644 --- a/sql/remove-all-schemas.sql +++ b/sql/remove-all-schemas.sql @@ -4,6 +4,7 @@ begin drop schema auth cascade; drop schema engine0 cascade; drop schema i18n cascade; +drop schema task cascade; drop schema util cascade; commit diff --git a/sql/revert/006-primary-location-for-instance.sql b/sql/revert/006-primary-location-for-instance.sql new file mode 100644 index 0000000..63e1928 --- /dev/null +++ b/sql/revert/006-primary-location-for-instance.sql @@ -0,0 +1,9 @@ +-- Revert graphql:006-primary-location-for-instance from pg +begin +; + +drop function if exists legacy0.primary_location_for_instance +; + +commit +; diff --git a/sql/sqitch.plan b/sql/sqitch.plan index a8d3f3c..56ee86e 100644 --- a/sql/sqitch.plan +++ b/sql/sqitch.plan @@ -1,9 +1,10 @@ %syntax-version=1.0.0 %project=graphql -001-init 2025-02-11T20:07:14Z Will Ruggiano # initial commit -002-engine 2025-02-11T20:15:48Z Will Ruggiano # rules engine v0 -003-i18n 2025-02-11T21:14:41Z Will Ruggiano # add i18n schema -004-legacy 2025-02-11T21:17:23Z Will Ruggiano # add legacy (primary) entity utilities -005-runtime 2025-02-11T21:52:56Z Will Ruggiano # add runtime demo -@v0.1.0 2025-02-11T23:24:21Z Will Ruggiano # v0.1.0 - and so it begins +001-init 2025-02-11T20:07:14Z Will Ruggiano +002-engine 2025-02-11T20:15:48Z Will Ruggiano +003-i18n 2025-02-11T21:14:41Z Will Ruggiano +004-legacy 2025-02-11T21:17:23Z Will Ruggiano +005-runtime 2025-02-11T21:52:56Z Will Ruggiano +006-primary-location-for-instance 2025-02-12T05:09:57Z Will Ruggiano +@v0.1.0 2025-02-12T06:27:02Z Will Ruggiano # v0.1.0 diff --git a/sql/verify/006-primary-location-for-instance.sql b/sql/verify/006-primary-location-for-instance.sql new file mode 100644 index 0000000..fc6331e --- /dev/null +++ b/sql/verify/006-primary-location-for-instance.sql @@ -0,0 +1,12 @@ +-- Verify graphql:006-primary-location-for-instance on pg +begin +; + +select + pg_catalog.has_function_privilege( + 'legacy0.primary_location_for_instance'::regproc, 'execute' + ) +; + +rollback +;