diff --git a/CHANGELOG.md b/CHANGELOG.md index 17acead..434c89b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,56 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning: ## [Unreleased] +### Changed +- Upgraded all templates to `@objectstack/* ^7.4.1` (from `^7.3.0`). +- **Approvals migrated to the 7.4.x flow-node model (ADR-0019).** The standalone + `ApprovalProcess` export and the stack-level `approvals:` key were removed in + 7.4.x; approvals are now an `approval` node on a `record_change` flow whose + `approve` / `reject` out-edges carry the decision branches. Reworked + `packages/expense` (`expense_approval.flow.ts`) and `packages/content` + (`publish_approval.flow.ts`) accordingly and deleted their `src/approvals/` + directories. +- **Repaired `packages/project`** (previously did not typecheck): rewrote both + state machines to the canonical `StateMachineConfig` (XState) shape and + `stateMachines: { lifecycle }` wiring; fixed flow `type: 'scheduled'` → + `'schedule'` and converted condition-node out-edges to + `type: 'conditional'` + `condition` / `isDefault`; moved view `filter` out of + the `data` provider to the view-level array form; normalised ESM relative + imports to `.js`; and wired the three flows back into the stack. + +- **Flow capability re-evaluation against the 7.4.x node registry.** + - Notifications now use the real builtin **`notify`** node (delivers via the + messaging service — inbox/email/push) instead of `script` + + `actionType: 'notification'`, which the 7.4.x script executor treats as a + **no-op** (log only). Migrated all 21 notification nodes + 2 templated-email + nodes (`actionType: 'email'`, also a log-only stub → `notify` with + `channels: ['email']`) across todo, expense, content, contracts, helpdesk, + hr, procurement, compliance, project. `link:` → `actionUrl:` to match the + `notify` config. + - `packages/project` flows used node types with **no runtime executor** + (`query`, `foreach`, `condition`) — they built but could never run. Rewrote + to the registered builtins (`get_record`, `loop`, `decision`) with + object-form filters, and wired the three flows into the stack. (The AI / + scheduled-scan logic in these flows remains a documented v0 stub: + `invoke_function` targets and per-item iteration depend on engine features + not yet wired.) + +- **QA scenario fixtures rewritten + runnable.** `packages/todo` and + `packages/hr` shipped obsolete `qa/*.test.json` (both were copies of an old + todo suite targeting removed objects `project`/`task`/`task_label`). Rewrote + them against the real schemas (todo: `todo_task`/`todo_label` lifecycle, + labels, due-date round-trip; hr: `hr_employee` onboarding, `hr_time_off_request` + draft→submitted→approved, `hr_document`). Added `scripts/run-qa.mjs`, a small + runner that authenticates (better-auth sign-up) and executes the scenarios + against the versioned data API — needed because the `objectstack test` adapter + bundled in `@objectstack/core` 7.4.x targets `/api/data/` while the + 7.4.x REST plugin serves `/api/v1/data/` (so the bundled runner 404s). + Both suites pass green (`pnpm --filter @objectlab/ test:qa` against a + running `objectstack dev`). Each template's `test` script now runs + `objectstack build` (the schema/protocol validation gate). + +All nine templates pass `tsc --noEmit` and `objectstack build` on 7.4.1. + ### Added - `packages/expense` — employee expense & reimbursement template (v0). 3 objects (`report`, `line`, `category`), report lifecycle state machine diff --git a/packages/compliance/package.json b/packages/compliance/package.json index caaf112..2d878db 100644 --- a/packages/compliance/package.json +++ b/packages/compliance/package.json @@ -15,20 +15,20 @@ "start": "objectstack start -p 4005", "build": "objectstack build", "typecheck": "tsc --noEmit", - "test": "objectstack test" + "test": "objectstack build" }, "dependencies": { - "@objectstack/account": "^7.3.0", - "@objectstack/cli": "^7.3.0", - "@objectstack/driver-memory": "^7.3.0", - "@objectstack/driver-sql": "^7.3.0", - "@objectstack/driver-sqlite-wasm": "^7.3.0", - "@objectstack/metadata": "^7.3.0", - "@objectstack/objectql": "^7.3.0", - "@objectstack/runtime": "^7.3.0", - "@objectstack/service-analytics": "^7.3.0", - "@objectstack/service-automation": "^7.3.0", - "@objectstack/spec": "^7.3.0", + "@objectstack/account": "^7.4.1", + "@objectstack/cli": "^7.4.1", + "@objectstack/driver-memory": "^7.4.1", + "@objectstack/driver-sql": "^7.4.1", + "@objectstack/driver-sqlite-wasm": "^7.4.1", + "@objectstack/metadata": "^7.4.1", + "@objectstack/objectql": "^7.4.1", + "@objectstack/runtime": "^7.4.1", + "@objectstack/service-analytics": "^7.4.1", + "@objectstack/service-automation": "^7.4.1", + "@objectstack/spec": "^7.4.1", "sql.js": "^1.14.1" }, "optionalDependencies": { diff --git a/packages/compliance/src/flows/evidence_auto_expire.flow.ts b/packages/compliance/src/flows/evidence_auto_expire.flow.ts index 1ea2bc7..e2d1bb6 100644 --- a/packages/compliance/src/flows/evidence_auto_expire.flow.ts +++ b/packages/compliance/src/flows/evidence_auto_expire.flow.ts @@ -22,8 +22,7 @@ export const EvidenceAutoExpireFlow: Flow = { label: 'Start', config: { objectName: 'compliance_evidence', - criteria: - 'status == "approved" && expires_on != null && expires_on == daysAgo(0)', + criteria: 'status == "approved" && expires_on != null && expires_on == daysAgo(0)', criteriaDialect: 'cel', }, }, diff --git a/packages/compliance/src/flows/evidence_expiring.flow.ts b/packages/compliance/src/flows/evidence_expiring.flow.ts index 619f06c..f987c5f 100644 --- a/packages/compliance/src/flows/evidence_expiring.flow.ts +++ b/packages/compliance/src/flows/evidence_expiring.flow.ts @@ -40,15 +40,13 @@ export const EvidenceExpiringFlow: Flow = { }, { id: 'notify', - type: 'script', + type: 'notify', label: 'Notify Collector', config: { - actionType: 'notification', recipients: ['{ev.collected_by}'], title: 'Evidence expiring soon: {ev.title}', - body: - 'Evidence "{ev.title}" expires on {ev.expires_on}. Collect a fresh copy and resubmit.', - link: '/objects/compliance_evidence/{ev.id}', + body: 'Evidence "{ev.title}" expires on {ev.expires_on}. Collect a fresh copy and resubmit.', + actionUrl: '/objects/compliance_evidence/{ev.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/compliance/src/flows/failed_control_escalation.flow.ts b/packages/compliance/src/flows/failed_control_escalation.flow.ts index 186d6e0..cd14076 100644 --- a/packages/compliance/src/flows/failed_control_escalation.flow.ts +++ b/packages/compliance/src/flows/failed_control_escalation.flow.ts @@ -50,15 +50,13 @@ export const FailedControlEscalationFlow: Flow = { }, { id: 'notify_owner', - type: 'script', + type: 'notify', label: 'Notify Control Owner', config: { - actionType: 'notification', recipients: ['{ctrl.owner}', 'role:compliance_admin'], title: 'Failed control: {ctrl.code} {ctrl.title}', - body: - 'Assessment "{asmt.title}" on {ctrl.code} ({ctrl.criticality}) FAILED. Remediation due {asmt.remediation_due}.', - link: '/objects/compliance_assessment/{asmt.id}', + body: 'Assessment "{asmt.title}" on {ctrl.code} ({ctrl.criticality}) FAILED. Remediation due {asmt.remediation_due}.', + actionUrl: '/objects/compliance_assessment/{asmt.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/content/objectstack.config.ts b/packages/content/objectstack.config.ts index 669d135..571d370 100644 --- a/packages/content/objectstack.config.ts +++ b/packages/content/objectstack.config.ts @@ -11,7 +11,6 @@ import * as apps from './src/apps/index.js'; import { ContentTranslations } from './src/translations/index.js'; import { allFlows } from './src/flows/index.js'; import { allHooks } from './src/hooks/index.js'; -import { allApprovals } from './src/approvals/index.js'; import { allSharingRules, RoleHierarchy } from './src/sharing/index.js'; import { ContentSeedData } from './src/data/index.js'; @@ -38,7 +37,6 @@ export default defineStack({ apps: Object.values(apps), flows: allFlows, hooks: allHooks, - approvals: allApprovals, translations: [ContentTranslations], sharingRules: allSharingRules, diff --git a/packages/content/package.json b/packages/content/package.json index c946a38..65f3196 100644 --- a/packages/content/package.json +++ b/packages/content/package.json @@ -15,20 +15,20 @@ "start": "objectstack start -p 4008", "build": "objectstack build", "typecheck": "tsc --noEmit", - "test": "objectstack test" + "test": "objectstack build" }, "dependencies": { - "@objectstack/account": "^7.3.0", - "@objectstack/cli": "^7.3.0", - "@objectstack/driver-memory": "^7.3.0", - "@objectstack/driver-sql": "^7.3.0", - "@objectstack/driver-sqlite-wasm": "^7.3.0", - "@objectstack/metadata": "^7.3.0", - "@objectstack/objectql": "^7.3.0", - "@objectstack/runtime": "^7.3.0", - "@objectstack/service-analytics": "^7.3.0", - "@objectstack/service-automation": "^7.3.0", - "@objectstack/spec": "^7.3.0", + "@objectstack/account": "^7.4.1", + "@objectstack/cli": "^7.4.1", + "@objectstack/driver-memory": "^7.4.1", + "@objectstack/driver-sql": "^7.4.1", + "@objectstack/driver-sqlite-wasm": "^7.4.1", + "@objectstack/metadata": "^7.4.1", + "@objectstack/objectql": "^7.4.1", + "@objectstack/runtime": "^7.4.1", + "@objectstack/service-analytics": "^7.4.1", + "@objectstack/service-automation": "^7.4.1", + "@objectstack/spec": "^7.4.1", "sql.js": "^1.14.1" }, "optionalDependencies": { diff --git a/packages/content/src/approvals/index.ts b/packages/content/src/approvals/index.ts deleted file mode 100644 index cffd0d2..0000000 --- a/packages/content/src/approvals/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. - -export { PublishApprovalProcess } from './publish_approval.approval'; -import { PublishApprovalProcess } from './publish_approval.approval'; - -export const allApprovals = [PublishApprovalProcess]; diff --git a/packages/content/src/approvals/publish_approval.approval.ts b/packages/content/src/approvals/publish_approval.approval.ts deleted file mode 100644 index 91876ac..0000000 --- a/packages/content/src/approvals/publish_approval.approval.ts +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. - -import type { ApprovalProcess } from '@objectstack/spec/automation'; - -/** - * publish_approval — gating the `in_review → approved` transition on a - * content piece. One step: the editorial lead role approves; rejection - * sends the piece back to the previous status (drafting) so the writer - * can iterate. - * - * Entry criteria: a piece reaches `in_review`. The state-machine APPROVE - * transition is wired to this process's onApprove side effect. - */ -export const PublishApprovalProcess: ApprovalProcess = { - name: 'publish_approval', - label: 'Publish Approval', - object: 'content_piece', - description: - 'Editorial-lead sign-off required before a piece moves from in_review to approved. Rejection bounces back to drafting.', - active: true, - entryCriteria: { dialect: 'cel', source: 'status == "in_review"' }, - lockRecord: true, - approvalStatusField: 'status', - - steps: [ - { - name: 'lead_signoff', - label: 'Editorial Lead Sign-off', - description: 'The editorial lead reviews the draft and approves or requests changes.', - approvers: [{ type: 'role', value: 'lead' }], - behavior: 'first_response', - rejectionBehavior: 'back_to_previous', - onApprove: [ - { - name: 'set_status_approved', - type: 'field_update', - config: { field: 'status', value: 'approved' }, - }, - ], - onReject: [ - { - name: 'set_status_drafting', - type: 'field_update', - config: { field: 'status', value: 'drafting' }, - }, - { - name: 'notify_writer', - type: 'inbox_notify', - config: { - recipients: ['{record.assignee}'], - title: 'Changes requested: {record.title}', - body: 'Your piece needs revisions before it can be approved.', - link: '/objects/content_piece/{record.id}', - }, - }, - ], - }, - ], - - onFinalApprove: [ - { - name: 'notify_writer_approved', - type: 'inbox_notify', - config: { - recipients: ['{record.assignee}'], - title: 'Approved: {record.title}', - body: 'Your piece is approved. Schedule it whenever ready.', - link: '/objects/content_piece/{record.id}', - }, - }, - ], - - escalation: { - enabled: true, - timeoutHours: 72, - action: 'notify', - notifySubmitter: true, - }, -}; diff --git a/packages/content/src/flows/index.ts b/packages/content/src/flows/index.ts index c382592..2e881f5 100644 --- a/packages/content/src/flows/index.ts +++ b/packages/content/src/flows/index.ts @@ -2,12 +2,14 @@ import { SignalToTopicPromotionFlow } from './signal_to_topic_promotion.flow'; import { CtaCreationDefaultFlow } from './cta_creation_default.flow'; +import { PublishApprovalFlow } from './publish_approval.flow'; import { PublicationRollupFlow } from './publication_rollup.flow'; import { StampLifecycleTimestampsFlow } from './stamp_lifecycle_timestamps.flow'; export const allFlows = [ SignalToTopicPromotionFlow, CtaCreationDefaultFlow, + PublishApprovalFlow, PublicationRollupFlow, StampLifecycleTimestampsFlow, ]; diff --git a/packages/content/src/flows/publication_rollup.flow.ts b/packages/content/src/flows/publication_rollup.flow.ts index e18955c..28cbdb1 100644 --- a/packages/content/src/flows/publication_rollup.flow.ts +++ b/packages/content/src/flows/publication_rollup.flow.ts @@ -34,7 +34,8 @@ export const PublicationRollupFlow: Flow = { label: 'Start', config: { objectName: 'content_metric', - criteria: 'ISCHANGED(views) OR ISCHANGED(clicks) OR ISCHANGED(signups) OR ISCHANGED(revenue) OR ISNEW()', + criteria: + 'ISCHANGED(views) OR ISCHANGED(clicks) OR ISCHANGED(signups) OR ISCHANGED(revenue) OR ISNEW()', }, }, { diff --git a/packages/content/src/flows/publish_approval.flow.ts b/packages/content/src/flows/publish_approval.flow.ts new file mode 100644 index 0000000..4696425 --- /dev/null +++ b/packages/content/src/flows/publish_approval.flow.ts @@ -0,0 +1,121 @@ +// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. + +import type * as Automation from '@objectstack/spec/automation'; +type Flow = Automation.Flow; + +/** + * Publish Approval — the 7.4.x approval model (ADR-0019): an `approval` node on + * a record-change flow replaces the old standalone ApprovalProcess. The node + * opens an approval request when a piece reaches `in_review`, locks the record, + * and suspends the run until the editorial lead decides. It then resumes down + * its `approve` or `reject` out-edge. + * + * approve → status = approved, notify the writer + * reject → status = drafting (bounce back), notify the writer to revise + * + * Entry criteria (status == in_review) lives on the start node; on-decision + * side effects live on the nodes wired to the approve/reject edges. + */ +export const PublishApprovalFlow: Flow = { + name: 'content_publish_approval', + label: 'Publish Approval', + description: + 'Editorial-lead sign-off gates in_review → approved. Rejection bounces the piece back to drafting.', + type: 'record_change', + + variables: [{ name: 'pieceId', type: 'text', isInput: true, isOutput: false }], + + nodes: [ + { + id: 'start', + type: 'start', + label: 'Start', + config: { + objectName: 'content_piece', + criteria: 'status == "in_review"', + criteriaDialect: 'cel', + }, + }, + { + id: 'lead_signoff', + type: 'approval', + label: 'Editorial Lead Sign-off', + config: { + approvers: [{ type: 'role', value: 'lead' }], + behavior: 'first_response', + lockRecord: true, + escalation: { + enabled: true, + timeoutHours: 72, + action: 'notify', + notifySubmitter: true, + }, + }, + }, + { + id: 'set_approved', + type: 'update_record', + label: 'Mark Approved', + config: { + objectName: 'content_piece', + recordId: '{pieceId}', + values: { status: 'approved' }, + }, + }, + { + id: 'notify_approved', + type: 'notify', + label: 'Notify Writer — Approved', + config: { + recipients: ['{record.assignee}'], + title: 'Approved: {record.title}', + body: 'Your piece is approved. Schedule it whenever ready.', + actionUrl: '/objects/content_piece/{record.id}', + }, + }, + { + id: 'set_drafting', + type: 'update_record', + label: 'Send Back to Drafting', + config: { + objectName: 'content_piece', + recordId: '{pieceId}', + values: { status: 'drafting' }, + }, + }, + { + id: 'notify_writer', + type: 'notify', + label: 'Notify Writer — Changes Requested', + config: { + recipients: ['{record.assignee}'], + title: 'Changes requested: {record.title}', + body: 'Your piece needs revisions before it can be approved.', + actionUrl: '/objects/content_piece/{record.id}', + }, + }, + { id: 'end', type: 'end', label: 'End' }, + ], + + edges: [ + { id: 'e1', source: 'start', target: 'lead_signoff', type: 'default' }, + { + id: 'e2', + source: 'lead_signoff', + target: 'set_approved', + type: 'conditional', + label: 'approve', + }, + { id: 'e3', source: 'set_approved', target: 'notify_approved', type: 'default' }, + { id: 'e4', source: 'notify_approved', target: 'end', type: 'default' }, + { + id: 'e5', + source: 'lead_signoff', + target: 'set_drafting', + type: 'conditional', + label: 'reject', + }, + { id: 'e6', source: 'set_drafting', target: 'notify_writer', type: 'default' }, + { id: 'e7', source: 'notify_writer', target: 'end', type: 'default' }, + ], +}; diff --git a/packages/content/src/flows/signal_to_topic_promotion.flow.ts b/packages/content/src/flows/signal_to_topic_promotion.flow.ts index ffa7fe4..7a56445 100644 --- a/packages/content/src/flows/signal_to_topic_promotion.flow.ts +++ b/packages/content/src/flows/signal_to_topic_promotion.flow.ts @@ -47,8 +47,7 @@ export const SignalToTopicPromotionFlow: Flow = { config: { objectName: 'content_topic', values: { - title: - '{signal.recommended_topic_title}', + title: '{signal.recommended_topic_title}', brief: 'Promoted from signal "{signal.headline}". Source: {signal.source_url}.\n\n{signal.summary}', pillar: 'industry_insight', @@ -72,15 +71,13 @@ export const SignalToTopicPromotionFlow: Flow = { }, { id: 'notify', - type: 'script', + type: 'notify', label: 'Notify Owner', config: { - actionType: 'notification', recipients: ['{signal.created_by}'], title: 'Topic ready: {topic.title}', - body: - 'Signal "{signal.headline}" was promoted into a new topic. Draft when you have a slot.', - link: '/objects/content_topic/{topic.id}', + body: 'Signal "{signal.headline}" was promoted into a new topic. Draft when you have a slot.', + actionUrl: '/objects/content_topic/{topic.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/content/src/flows/stamp_lifecycle_timestamps.flow.ts b/packages/content/src/flows/stamp_lifecycle_timestamps.flow.ts index fccba4d..383f1c8 100644 --- a/packages/content/src/flows/stamp_lifecycle_timestamps.flow.ts +++ b/packages/content/src/flows/stamp_lifecycle_timestamps.flow.ts @@ -51,14 +51,13 @@ export const StampLifecycleTimestampsFlow: Flow = { }, { id: 'notify_editor', - type: 'script', + type: 'notify', label: 'Notify Editor', config: { - actionType: 'notification', recipients: ['{piece.editor}'], title: 'Review ready: {piece.title}', body: '{piece.assignee} submitted "{piece.title}" for your review.', - link: '/objects/content_piece/{piece.id}', + actionUrl: '/objects/content_piece/{piece.id}', }, }, { @@ -71,14 +70,13 @@ export const StampLifecycleTimestampsFlow: Flow = { }, { id: 'notify_published', - type: 'script', + type: 'notify', label: 'Notify Writer', config: { - actionType: 'notification', recipients: ['{piece.assignee}'], title: 'Live: {piece.title}', body: '"{piece.title}" is published. Metrics will appear after the first snapshot is recorded.', - link: '/objects/content_piece/{piece.id}', + actionUrl: '/objects/content_piece/{piece.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 81f3ee3..4066bf0 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -15,20 +15,20 @@ "start": "objectstack start -p 4003", "build": "objectstack build", "typecheck": "tsc --noEmit", - "test": "objectstack test" + "test": "objectstack build" }, "dependencies": { - "@objectstack/account": "^7.3.0", - "@objectstack/cli": "^7.3.0", - "@objectstack/driver-memory": "^7.3.0", - "@objectstack/driver-sql": "^7.3.0", - "@objectstack/driver-sqlite-wasm": "^7.3.0", - "@objectstack/metadata": "^7.3.0", - "@objectstack/objectql": "^7.3.0", - "@objectstack/runtime": "^7.3.0", - "@objectstack/service-analytics": "^7.3.0", - "@objectstack/service-automation": "^7.3.0", - "@objectstack/spec": "^7.3.0", + "@objectstack/account": "^7.4.1", + "@objectstack/cli": "^7.4.1", + "@objectstack/driver-memory": "^7.4.1", + "@objectstack/driver-sql": "^7.4.1", + "@objectstack/driver-sqlite-wasm": "^7.4.1", + "@objectstack/metadata": "^7.4.1", + "@objectstack/objectql": "^7.4.1", + "@objectstack/runtime": "^7.4.1", + "@objectstack/service-analytics": "^7.4.1", + "@objectstack/service-automation": "^7.4.1", + "@objectstack/spec": "^7.4.1", "sql.js": "^1.14.1" }, "optionalDependencies": { diff --git a/packages/contracts/src/flows/obligation_overdue.flow.ts b/packages/contracts/src/flows/obligation_overdue.flow.ts index ca74f31..eac4f14 100644 --- a/packages/contracts/src/flows/obligation_overdue.flow.ts +++ b/packages/contracts/src/flows/obligation_overdue.flow.ts @@ -38,15 +38,13 @@ export const ObligationOverdueFlow: Flow = { }, { id: 'notify', - type: 'script', + type: 'notify', label: 'Notify Assignee', config: { - actionType: 'notification', recipients: ['{obligationRecord.assignee}'], title: 'Obligation overdue: {obligationRecord.summary}', - body: - 'Obligation "{obligationRecord.summary}" on contract {obligationRecord.contract} is past its due date ({obligationRecord.due_date}).', - link: '/objects/contracts_obligation/{obligationRecord.id}', + body: 'Obligation "{obligationRecord.summary}" on contract {obligationRecord.contract} is past its due date ({obligationRecord.due_date}).', + actionUrl: '/objects/contracts_obligation/{obligationRecord.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/contracts/src/flows/renewal_alert.flow.ts b/packages/contracts/src/flows/renewal_alert.flow.ts index 885b3d1..f9b1e75 100644 --- a/packages/contracts/src/flows/renewal_alert.flow.ts +++ b/packages/contracts/src/flows/renewal_alert.flow.ts @@ -48,31 +48,25 @@ export const ContractRenewalAlertFlow: Flow = { }, { id: 'notify', - type: 'script', + type: 'notify', label: 'Notify Owner', config: { - actionType: 'notification', recipients: ['{contractRecord.owner}'], title: 'Contract renewing soon: {contractRecord.title}', - body: - 'Contract "{contractRecord.title}" with {contractRecord.party} reaches its end_date on {contractRecord.end_date}. Auto-renew is {contractRecord.auto_renew}. Review now.', - link: '/objects/contracts_contract/{contractRecord.id}', + body: 'Contract "{contractRecord.title}" with {contractRecord.party} reaches its end_date on {contractRecord.end_date}. Auto-renew is {contractRecord.auto_renew}. Review now.', + actionUrl: '/objects/contracts_contract/{contractRecord.id}', }, }, { id: 'email', - type: 'script', + type: 'notify', label: 'Email Owner', config: { - actionType: 'email', - template: 'contracts_renewal_alert', + channels: ['email'], recipients: ['{contractRecord.owner.email}'], - variables: { - title: '{contractRecord.title}', - party: '{contractRecord.party}', - endDate: '{contractRecord.end_date}', - autoRenew: '{contractRecord.auto_renew}', - }, + title: 'Contract renewing soon: {contractRecord.title}', + body: 'Contract "{contractRecord.title}" with {contractRecord.party} reaches its end_date on {contractRecord.end_date}. Auto-renew is {contractRecord.auto_renew}.', + actionUrl: '/objects/contracts_contract/{contractRecord.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/contracts/src/flows/renewal_draft.flow.ts b/packages/contracts/src/flows/renewal_draft.flow.ts index 47adbd9..2fc3899 100644 --- a/packages/contracts/src/flows/renewal_draft.flow.ts +++ b/packages/contracts/src/flows/renewal_draft.flow.ts @@ -22,7 +22,7 @@ export const ContractRenewalDraftFlow: Flow = { name: 'contracts_contract_renewal_draft', label: 'Create Renewal Draft Before End Date', description: - 'When a non-auto-renewing active contract is within its renewal-notice window, drop a draft renewal into the owner\'s queue.', + "When a non-auto-renewing active contract is within its renewal-notice window, drop a draft renewal into the owner's queue.", type: 'record_change', variables: [{ name: 'contractId', type: 'text', isInput: true, isOutput: false }], @@ -72,15 +72,13 @@ export const ContractRenewalDraftFlow: Flow = { }, { id: 'notify_owner', - type: 'script', + type: 'notify', label: 'Notify Owner', config: { - actionType: 'notification', recipients: ['{parent.owner}'], title: 'Renewal draft ready: {parent.title}', - body: - 'A draft renewal for "{parent.title}" was prepared. End date approaching ({parent.end_date}). Review and send.', - link: '/objects/contracts_contract/{draft.id}', + body: 'A draft renewal for "{parent.title}" was prepared. End date approaching ({parent.end_date}). Review and send.', + actionUrl: '/objects/contracts_contract/{draft.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/expense/objectstack.config.ts b/packages/expense/objectstack.config.ts index ac95dd1..b81500a 100644 --- a/packages/expense/objectstack.config.ts +++ b/packages/expense/objectstack.config.ts @@ -11,7 +11,6 @@ import * as apps from './src/apps/index.js'; import { ExpenseTranslations } from './src/translations/index.js'; import { allFlows } from './src/flows/index.js'; import { allHooks } from './src/hooks/index.js'; -import { allApprovals } from './src/approvals/index.js'; import { RoleHierarchy } from './src/sharing/index.js'; import { ExpenseSeedData } from './src/data/index.js'; @@ -36,7 +35,6 @@ export default defineStack({ apps: Object.values(apps), flows: allFlows, hooks: allHooks, - approvals: allApprovals, translations: [ExpenseTranslations], sharingRules: [], diff --git a/packages/expense/package.json b/packages/expense/package.json index 9c0aa58..6584f3a 100644 --- a/packages/expense/package.json +++ b/packages/expense/package.json @@ -15,20 +15,20 @@ "start": "objectstack start -p 4011", "build": "objectstack build", "typecheck": "tsc --noEmit", - "test": "objectstack test" + "test": "objectstack build" }, "dependencies": { - "@objectstack/account": "^7.3.0", - "@objectstack/cli": "^7.3.0", - "@objectstack/driver-memory": "^7.3.0", - "@objectstack/driver-sql": "^7.3.0", - "@objectstack/driver-sqlite-wasm": "^7.3.0", - "@objectstack/metadata": "^7.3.0", - "@objectstack/objectql": "^7.3.0", - "@objectstack/runtime": "^7.3.0", - "@objectstack/service-analytics": "^7.3.0", - "@objectstack/service-automation": "^7.3.0", - "@objectstack/spec": "^7.3.0", + "@objectstack/account": "^7.4.1", + "@objectstack/cli": "^7.4.1", + "@objectstack/driver-memory": "^7.4.1", + "@objectstack/driver-sql": "^7.4.1", + "@objectstack/driver-sqlite-wasm": "^7.4.1", + "@objectstack/metadata": "^7.4.1", + "@objectstack/objectql": "^7.4.1", + "@objectstack/runtime": "^7.4.1", + "@objectstack/service-analytics": "^7.4.1", + "@objectstack/service-automation": "^7.4.1", + "@objectstack/spec": "^7.4.1", "sql.js": "^1.14.1" }, "optionalDependencies": { diff --git a/packages/expense/src/approvals/expense_approval.approval.ts b/packages/expense/src/approvals/expense_approval.approval.ts deleted file mode 100644 index 1fd9c1e..0000000 --- a/packages/expense/src/approvals/expense_approval.approval.ts +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. - -import type { ApprovalProcess } from '@objectstack/spec/automation'; - -/** - * expense_approval — gates the `submitted → approved` transition on an - * expense report. One step: the expense-manager role approves; rejection - * sends the report to `rejected` so the employee can reopen and fix it. - * - * Entry criteria: a report reaches `submitted`. The state-machine APPROVE - * transition is wired to this process's onApprove side effect. - */ -export const ExpenseApprovalProcess: ApprovalProcess = { - name: 'expense_approval', - label: 'Expense Approval', - object: 'expense_report', - description: - 'Manager sign-off required before a report moves from submitted to approved. Rejection sends it back to the employee.', - active: true, - entryCriteria: { dialect: 'cel', source: 'status == "submitted"' }, - lockRecord: true, - approvalStatusField: 'status', - - steps: [ - { - name: 'manager_signoff', - label: 'Manager Sign-off', - description: 'The expense manager reviews the report and approves or rejects.', - approvers: [{ type: 'role', value: 'expense_manager' }], - behavior: 'first_response', - rejectionBehavior: 'reject_process', - onApprove: [ - { - name: 'set_status_approved', - type: 'field_update', - config: { field: 'status', value: 'approved' }, - }, - ], - onReject: [ - { - name: 'set_status_rejected', - type: 'field_update', - config: { field: 'status', value: 'rejected' }, - }, - { - name: 'notify_employee_rejected', - type: 'inbox_notify', - config: { - recipients: ['{record.requester}'], - title: 'Changes requested: {record.title}', - body: 'Your expense report was sent back. Review the notes, fix it, and resubmit.', - link: '/objects/expense_report/{record.id}', - }, - }, - ], - }, - ], - - onFinalApprove: [ - { - name: 'notify_employee_approved', - type: 'inbox_notify', - config: { - recipients: ['{record.requester}'], - title: 'Approved: {record.title}', - body: 'Your expense report is approved and queued for reimbursement.', - link: '/objects/expense_report/{record.id}', - }, - }, - ], - - escalation: { - enabled: true, - timeoutHours: 72, - action: 'notify', - notifySubmitter: true, - }, -}; diff --git a/packages/expense/src/approvals/index.ts b/packages/expense/src/approvals/index.ts deleted file mode 100644 index 87c45b9..0000000 --- a/packages/expense/src/approvals/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. - -import { ExpenseApprovalProcess } from './expense_approval.approval'; - -export const allApprovals = [ExpenseApprovalProcess]; diff --git a/packages/expense/src/flows/expense_approval.flow.ts b/packages/expense/src/flows/expense_approval.flow.ts new file mode 100644 index 0000000..e5b94c8 --- /dev/null +++ b/packages/expense/src/flows/expense_approval.flow.ts @@ -0,0 +1,121 @@ +// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. + +import type * as Automation from '@objectstack/spec/automation'; +type Flow = Automation.Flow; + +/** + * Expense Approval — the 7.4.x approval model (ADR-0019): an `approval` node on + * a record-change flow replaces the old standalone ApprovalProcess. The node + * opens an approval request when a report reaches `submitted`, locks the record, + * and suspends the run until the expense-manager decides. It then resumes down + * its `approve` or `reject` out-edge. + * + * approve → status = approved, notify the employee + * reject → status = rejected, notify the employee to fix and resubmit + * + * Entry criteria (status == submitted) lives on the start node; on-decision side + * effects live on the nodes wired to the approve/reject edges. + */ +export const ExpenseApprovalFlow: Flow = { + name: 'expense_report_approval', + label: 'Expense Approval', + description: + 'Manager sign-off gates submitted → approved. Rejection sends the report back to the employee.', + type: 'record_change', + + variables: [{ name: 'reportId', type: 'text', isInput: true, isOutput: false }], + + nodes: [ + { + id: 'start', + type: 'start', + label: 'Start', + config: { + objectName: 'expense_report', + criteria: 'status == "submitted"', + criteriaDialect: 'cel', + }, + }, + { + id: 'manager_signoff', + type: 'approval', + label: 'Manager Sign-off', + config: { + approvers: [{ type: 'role', value: 'expense_manager' }], + behavior: 'first_response', + lockRecord: true, + escalation: { + enabled: true, + timeoutHours: 72, + action: 'notify', + notifySubmitter: true, + }, + }, + }, + { + id: 'set_approved', + type: 'update_record', + label: 'Mark Approved', + config: { + objectName: 'expense_report', + recordId: '{reportId}', + values: { status: 'approved' }, + }, + }, + { + id: 'notify_approved', + type: 'notify', + label: 'Notify Employee — Approved', + config: { + recipients: ['{record.requester}'], + title: 'Approved: {record.title}', + body: 'Your expense report is approved and queued for reimbursement.', + actionUrl: '/objects/expense_report/{record.id}', + }, + }, + { + id: 'set_rejected', + type: 'update_record', + label: 'Mark Rejected', + config: { + objectName: 'expense_report', + recordId: '{reportId}', + values: { status: 'rejected' }, + }, + }, + { + id: 'notify_rejected', + type: 'notify', + label: 'Notify Employee — Changes Requested', + config: { + recipients: ['{record.requester}'], + title: 'Changes requested: {record.title}', + body: 'Your expense report was sent back. Review the notes, fix it, and resubmit.', + actionUrl: '/objects/expense_report/{record.id}', + }, + }, + { id: 'end', type: 'end', label: 'End' }, + ], + + edges: [ + { id: 'e1', source: 'start', target: 'manager_signoff', type: 'default' }, + { + id: 'e2', + source: 'manager_signoff', + target: 'set_approved', + type: 'conditional', + label: 'approve', + }, + { id: 'e3', source: 'set_approved', target: 'notify_approved', type: 'default' }, + { id: 'e4', source: 'notify_approved', target: 'end', type: 'default' }, + { + id: 'e5', + source: 'manager_signoff', + target: 'set_rejected', + type: 'conditional', + label: 'reject', + }, + { id: 'e6', source: 'set_rejected', target: 'notify_rejected', type: 'default' }, + { id: 'e7', source: 'notify_rejected', target: 'end', type: 'default' }, + ], +}; diff --git a/packages/expense/src/flows/expense_approval_overdue.flow.ts b/packages/expense/src/flows/expense_approval_overdue.flow.ts index 8d40614..8548290 100644 --- a/packages/expense/src/flows/expense_approval_overdue.flow.ts +++ b/packages/expense/src/flows/expense_approval_overdue.flow.ts @@ -41,15 +41,13 @@ export const ExpenseApprovalOverdueFlow: Flow = { }, { id: 'escalate', - type: 'script', + type: 'notify', label: 'Escalate', config: { - actionType: 'notification', recipients: ['role:expense_manager'], title: 'Still pending: {report.title}', - body: - 'Report "{report.title}" ({report.total_amount}) has awaited approval for 3 days. Please review.', - link: '/objects/expense_report/{report.id}', + body: 'Report "{report.title}" ({report.total_amount}) has awaited approval for 3 days. Please review.', + actionUrl: '/objects/expense_report/{report.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/expense/src/flows/expense_reimbursed.flow.ts b/packages/expense/src/flows/expense_reimbursed.flow.ts index 93cc0b7..11486b1 100644 --- a/packages/expense/src/flows/expense_reimbursed.flow.ts +++ b/packages/expense/src/flows/expense_reimbursed.flow.ts @@ -39,15 +39,13 @@ export const ExpenseReimbursedFlow: Flow = { }, { id: 'notify_employee', - type: 'script', + type: 'notify', label: 'Notify Employee', config: { - actionType: 'notification', recipients: ['{report.requester}'], title: 'Reimbursed: {report.title}', - body: - 'Your expense report "{report.title}" ({report.total_amount}) has been reimbursed via {report.payment_method}. Reference: {report.payment_reference}.', - link: '/objects/expense_report/{report.id}', + body: 'Your expense report "{report.title}" ({report.total_amount}) has been reimbursed via {report.payment_method}. Reference: {report.payment_reference}.', + actionUrl: '/objects/expense_report/{report.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/expense/src/flows/expense_submitted.flow.ts b/packages/expense/src/flows/expense_submitted.flow.ts index f1dc764..518c4b9 100644 --- a/packages/expense/src/flows/expense_submitted.flow.ts +++ b/packages/expense/src/flows/expense_submitted.flow.ts @@ -11,8 +11,7 @@ type Flow = Automation.Flow; export const ExpenseSubmittedFlow: Flow = { name: 'expense_report_submitted', label: 'Notify Approver on Submit', - description: - 'When an expense report is submitted, alert the expense-manager role to review it.', + description: 'When an expense report is submitted, alert the expense-manager role to review it.', type: 'record_change', variables: [{ name: 'reportId', type: 'text', isInput: true, isOutput: false }], @@ -40,15 +39,13 @@ export const ExpenseSubmittedFlow: Flow = { }, { id: 'notify_manager', - type: 'script', + type: 'notify', label: 'Notify Manager', config: { - actionType: 'notification', recipients: ['role:expense_manager'], title: 'Expense report awaiting approval: {report.title}', - body: - 'Report "{report.title}" for {report.total_amount} ({report.cost_center}) is awaiting your approval.', - link: '/objects/expense_report/{report.id}', + body: 'Report "{report.title}" for {report.total_amount} ({report.cost_center}) is awaiting your approval.', + actionUrl: '/objects/expense_report/{report.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/expense/src/flows/index.ts b/packages/expense/src/flows/index.ts index 9f9ac54..4a7c5ab 100644 --- a/packages/expense/src/flows/index.ts +++ b/packages/expense/src/flows/index.ts @@ -1,11 +1,13 @@ // Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. import { ExpenseSubmittedFlow } from './expense_submitted.flow'; +import { ExpenseApprovalFlow } from './expense_approval.flow'; import { ExpenseReimbursedFlow } from './expense_reimbursed.flow'; import { ExpenseApprovalOverdueFlow } from './expense_approval_overdue.flow'; export const allFlows = [ ExpenseSubmittedFlow, + ExpenseApprovalFlow, ExpenseReimbursedFlow, ExpenseApprovalOverdueFlow, ]; diff --git a/packages/helpdesk/package.json b/packages/helpdesk/package.json index 83d4e4f..09f229d 100644 --- a/packages/helpdesk/package.json +++ b/packages/helpdesk/package.json @@ -15,20 +15,20 @@ "start": "objectstack start -p 4006", "build": "objectstack build", "typecheck": "tsc --noEmit", - "test": "objectstack test" + "test": "objectstack build" }, "dependencies": { - "@objectstack/account": "^7.3.0", - "@objectstack/cli": "^7.3.0", - "@objectstack/driver-memory": "^7.3.0", - "@objectstack/driver-sql": "^7.3.0", - "@objectstack/driver-sqlite-wasm": "^7.3.0", - "@objectstack/metadata": "^7.3.0", - "@objectstack/objectql": "^7.3.0", - "@objectstack/runtime": "^7.3.0", - "@objectstack/service-analytics": "^7.3.0", - "@objectstack/service-automation": "^7.3.0", - "@objectstack/spec": "^7.3.0", + "@objectstack/account": "^7.4.1", + "@objectstack/cli": "^7.4.1", + "@objectstack/driver-memory": "^7.4.1", + "@objectstack/driver-sql": "^7.4.1", + "@objectstack/driver-sqlite-wasm": "^7.4.1", + "@objectstack/metadata": "^7.4.1", + "@objectstack/objectql": "^7.4.1", + "@objectstack/runtime": "^7.4.1", + "@objectstack/service-analytics": "^7.4.1", + "@objectstack/service-automation": "^7.4.1", + "@objectstack/spec": "^7.4.1", "sql.js": "^1.14.1" }, "optionalDependencies": { diff --git a/packages/helpdesk/src/flows/auto_close_resolved.flow.ts b/packages/helpdesk/src/flows/auto_close_resolved.flow.ts index cc91241..73bbdeb 100644 --- a/packages/helpdesk/src/flows/auto_close_resolved.flow.ts +++ b/packages/helpdesk/src/flows/auto_close_resolved.flow.ts @@ -14,8 +14,7 @@ type Flow = Automation.Flow; export const AutoCloseResolvedFlow: Flow = { name: 'helpdesk_auto_close_resolved', label: 'Auto-Close Long-Resolved Tickets', - description: - 'Close tickets that have been resolved for 7 days with no further customer reply.', + description: 'Close tickets that have been resolved for 7 days with no further customer reply.', type: 'record_change', variables: [{ name: 'ticketId', type: 'text', isInput: true, isOutput: false }], diff --git a/packages/helpdesk/src/flows/escalate_angry_customer.flow.ts b/packages/helpdesk/src/flows/escalate_angry_customer.flow.ts index 76f1eec..3ec729b 100644 --- a/packages/helpdesk/src/flows/escalate_angry_customer.flow.ts +++ b/packages/helpdesk/src/flows/escalate_angry_customer.flow.ts @@ -12,8 +12,7 @@ type Flow = Automation.Flow; export const EscalateAngryCustomerFlow: Flow = { name: 'helpdesk_escalate_angry_customer', label: 'Escalate When Customer is Angry', - description: - 'On angry sentiment + high/urgent priority, escalate to team manager.', + description: 'On angry sentiment + high/urgent priority, escalate to team manager.', type: 'record_change', variables: [{ name: 'ticketId', type: 'text', isInput: true, isOutput: false }], @@ -52,15 +51,13 @@ export const EscalateAngryCustomerFlow: Flow = { }, { id: 'notify_manager', - type: 'script', + type: 'notify', label: 'Notify Team Manager', config: { - actionType: 'notification', recipients: ['{ticket.team.manager}'], title: 'Angry customer: {ticket.ticket_number}', - body: - 'AI detected angry sentiment on {priority} ticket "{ticket.name}" from {ticket.customer}. Intervene now.', - link: '/objects/helpdesk_ticket/{ticket.id}', + body: 'AI detected angry sentiment on {priority} ticket "{ticket.name}" from {ticket.customer}. Intervene now.', + actionUrl: '/objects/helpdesk_ticket/{ticket.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/helpdesk/src/flows/sla_first_response_warn.flow.ts b/packages/helpdesk/src/flows/sla_first_response_warn.flow.ts index 7b54856..f5461b7 100644 --- a/packages/helpdesk/src/flows/sla_first_response_warn.flow.ts +++ b/packages/helpdesk/src/flows/sla_first_response_warn.flow.ts @@ -51,15 +51,13 @@ export const SlaFirstResponseWarnFlow: Flow = { }, { id: 'notify_assignee', - type: 'script', + type: 'notify', label: 'Notify Assignee', config: { - actionType: 'notification', recipients: ['{ticket.assignee}'], title: 'First-response SLA at risk: {ticket.ticket_number}', - body: - 'Ticket "{ticket.name}" from {ticket.customer} is near its first-response deadline ({ticket.first_response_due_at}). Reply now.', - link: '/objects/helpdesk_ticket/{ticket.id}', + body: 'Ticket "{ticket.name}" from {ticket.customer} is near its first-response deadline ({ticket.first_response_due_at}). Reply now.', + actionUrl: '/objects/helpdesk_ticket/{ticket.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/helpdesk/src/flows/sla_resolution_breach.flow.ts b/packages/helpdesk/src/flows/sla_resolution_breach.flow.ts index 8d7d1ee..f74f97d 100644 --- a/packages/helpdesk/src/flows/sla_resolution_breach.flow.ts +++ b/packages/helpdesk/src/flows/sla_resolution_breach.flow.ts @@ -51,15 +51,13 @@ export const SlaResolutionBreachFlow: Flow = { }, { id: 'notify_manager', - type: 'script', + type: 'notify', label: 'Notify Team Manager', config: { - actionType: 'notification', recipients: ['{ticket.team.manager}'], title: 'SLA breached: {ticket.ticket_number}', - body: - 'Ticket "{ticket.name}" missed its resolution SLA ({ticket.resolution_due_at}). Auto-escalated.', - link: '/objects/helpdesk_ticket/{ticket.id}', + body: 'Ticket "{ticket.name}" missed its resolution SLA ({ticket.resolution_due_at}). Auto-escalated.', + actionUrl: '/objects/helpdesk_ticket/{ticket.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/hr/package.json b/packages/hr/package.json index c455786..26962de 100644 --- a/packages/hr/package.json +++ b/packages/hr/package.json @@ -15,20 +15,21 @@ "start": "objectstack start -p 4009", "build": "objectstack build", "typecheck": "tsc --noEmit", - "test": "objectstack test" + "test": "objectstack build", + "test:qa": "node ../../scripts/run-qa.mjs --url http://localhost:4009 --file qa/business-workflow.test.json" }, "dependencies": { - "@objectstack/account": "^7.3.0", - "@objectstack/cli": "^7.3.0", - "@objectstack/driver-memory": "^7.3.0", - "@objectstack/driver-sql": "^7.3.0", - "@objectstack/driver-sqlite-wasm": "^7.3.0", - "@objectstack/metadata": "^7.3.0", - "@objectstack/objectql": "^7.3.0", - "@objectstack/runtime": "^7.3.0", - "@objectstack/service-analytics": "^7.3.0", - "@objectstack/service-automation": "^7.3.0", - "@objectstack/spec": "^7.3.0", + "@objectstack/account": "^7.4.1", + "@objectstack/cli": "^7.4.1", + "@objectstack/driver-memory": "^7.4.1", + "@objectstack/driver-sql": "^7.4.1", + "@objectstack/driver-sqlite-wasm": "^7.4.1", + "@objectstack/metadata": "^7.4.1", + "@objectstack/objectql": "^7.4.1", + "@objectstack/runtime": "^7.4.1", + "@objectstack/service-analytics": "^7.4.1", + "@objectstack/service-automation": "^7.4.1", + "@objectstack/spec": "^7.4.1", "sql.js": "^1.14.1" }, "optionalDependencies": { diff --git a/packages/hr/qa/business-workflow.test.json b/packages/hr/qa/business-workflow.test.json index f7b8fed..e720dac 100644 --- a/packages/hr/qa/business-workflow.test.json +++ b/packages/hr/qa/business-workflow.test.json @@ -1,581 +1,214 @@ { - "name": "Todo — End-to-end business user workflow", + "name": "HR — end-to-end business user workflow", + "description": "Exercises the hr template's real schema (hr_employee, hr_time_off_request, hr_document): employee onboarding, the time-off request state machine (draft → submitted → approved), and document tracking. Targets the versioned data API (/api/v1/data/).", "scenarios": [ { - "id": "S1-setup-project", - "name": "PM sets up a new project and seeds the first tasks", - "description": "Lead opens the Projects page, creates a project, then files three starter tasks against it. Verifies the project rollup count.", - "tags": ["smoke", "projects", "tasks"], + "id": "S1-employee-onboarding", + "name": "HR onboards a new employee", + "description": "Create an active full-time employee and verify the core identity fields persist.", + "tags": ["smoke", "employee"], "steps": [ { - "name": "create_project", + "name": "create_employee", "action": { "type": "create_record", - "target": "project", + "target": "hr_employee", "payload": { - "name": "QA Workflow Project", - "key": "QAW", - "description": "Created by the business-workflow QA scenario.", + "full_name": "Dana Okoye", + "work_email": "dana.okoye+{{runId}}@acme.test", "status": "active", - "color": "#3B82F6" + "employment_type": "full_time", + "job_title": "Product Designer" } }, - "capture": { "projectId": "id" }, - "assertions": [ - { "field": "name", "operator": "equals", "expectedValue": "QA Workflow Project" }, - { "field": "status", "operator": "equals", "expectedValue": "active" }, - { "field": "id", "operator": "not_null", "expectedValue": null } - ] - }, - { - "name": "create_first_task", - "action": { - "type": "create_record", - "target": "task", - "payload": { - "subject": "QAW — write product requirements", - "project": "{{projectId}}", - "status": "todo", - "priority": "high" - } - }, - "capture": { "task1Id": "id" }, - "assertions": [ - { "field": "status", "operator": "equals", "expectedValue": "todo" }, - { "field": "approval_status", "operator": "equals", "expectedValue": "not_required" } - ] - }, - { - "name": "create_second_task", - "action": { - "type": "create_record", - "target": "task", - "payload": { - "subject": "QAW — set up CI pipeline", - "project": "{{projectId}}", - "status": "todo", - "priority": "normal" - } - }, - "capture": { "task2Id": "id" } - }, - { - "name": "create_third_task", - "action": { - "type": "create_record", - "target": "task", - "payload": { - "subject": "QAW — schedule kickoff meeting", - "project": "{{projectId}}", - "status": "todo", - "priority": "low" + "capture": { + "employeeId": "id" + }, + "assertions": [ + { + "field": "record.full_name", + "operator": "equals", + "expectedValue": "Dana Okoye" + }, + { + "field": "record.status", + "operator": "equals", + "expectedValue": "active" + }, + { + "field": "record.id", + "operator": "not_null", + "expectedValue": null } - }, - "capture": { "task3Id": "id" } - }, - { - "name": "verify_project_rollup", - "description": "task_count summary on project should reflect the 3 new tasks.", - "action": { - "type": "read_record", - "target": "project", - "payload": { "id": "{{projectId}}" } - }, - "assertions": [ - { "field": "task_count", "operator": "gte", "expectedValue": 3 } ] } - ], - "teardown": [ - { - "name": "cleanup_task_1", - "action": { "type": "delete_record", "target": "task", "payload": { "id": "{{task1Id}}" } } - }, - { - "name": "cleanup_task_2", - "action": { "type": "delete_record", "target": "task", "payload": { "id": "{{task2Id}}" } } - }, - { - "name": "cleanup_task_3", - "action": { "type": "delete_record", "target": "task", "payload": { "id": "{{task3Id}}" } } - }, - { - "name": "cleanup_project", - "action": { "type": "delete_record", "target": "project", "payload": { "id": "{{projectId}}" } } - } ] }, - { - "id": "S2-task-lifecycle", - "name": "Contributor walks a task through todo → doing → done", - "description": "Verifies the task state machine, the started_at / completed_at field-update workflows, and that done tasks drop out of the active workload query.", - "tags": ["state-machine", "workflows"], + "id": "S2-time-off-lifecycle", + "name": "An employee's time-off request moves through approval", + "description": "Seed an employee, file a vacation request (draft), submit it, then approve it — verifying the status state machine.", + "tags": ["time-off", "state-machine"], "setup": [ { - "name": "seed_project", - "action": { - "type": "create_record", - "target": "project", - "payload": { "name": "QA Lifecycle Project", "key": "QAL", "status": "active" } - }, - "capture": { "projectId": "id" } - }, - { - "name": "seed_task", + "name": "seed_employee", "action": { "type": "create_record", - "target": "task", - "payload": { - "subject": "QAL — implement search box", - "project": "{{projectId}}", - "status": "todo", - "priority": "normal", - "estimate_hours": 4 - } - }, - "capture": { "taskId": "id" } - } - ], - "steps": [ - { - "name": "start_work", - "description": "todo → doing should stamp started_at via the on_update workflow.", - "action": { - "type": "update_record", - "target": "task", - "payload": { "id": "{{taskId}}", "status": "doing" } - }, - "assertions": [ - { "field": "status", "operator": "equals", "expectedValue": "doing" }, - { "field": "started_at", "operator": "not_null", "expectedValue": null } - ] - }, - { - "name": "complete_work", - "description": "doing → done should stamp completed_at.", - "action": { - "type": "update_record", - "target": "task", - "payload": { "id": "{{taskId}}", "status": "done" } - }, - "assertions": [ - { "field": "status", "operator": "equals", "expectedValue": "done" }, - { "field": "completed_at", "operator": "not_null", "expectedValue": null } - ] - }, - { - "name": "active_workload_excludes_done", - "description": "An active-workload query (status in todo/doing) must not return the completed task.", - "action": { - "type": "query_records", - "target": "task", + "target": "hr_employee", "payload": { - "filter": { - "project": "{{projectId}}", - "status": { "$in": ["todo", "doing"] } - } + "full_name": "Sam Rivera", + "work_email": "sam.rivera+{{runId}}@acme.test", + "status": "active", + "employment_type": "full_time", + "job_title": "Software Engineer" } }, - "assertions": [ - { "field": "data.length", "operator": "equals", "expectedValue": 0 } - ] - } - ], - "teardown": [ - { - "name": "cleanup_task", - "action": { "type": "delete_record", "target": "task", "payload": { "id": "{{taskId}}" } } - }, - { - "name": "cleanup_project", - "action": { "type": "delete_record", "target": "project", "payload": { "id": "{{projectId}}" } } - } - ] - }, - - { - "id": "S3-urgent-approval", - "name": "Urgent task enters the approval process", - "description": "Creating an urgent task should set approval_status to pending (entryCriteria of UrgentTaskApproval).", - "tags": ["approvals"], - "setup": [ - { - "name": "seed_project", - "action": { - "type": "create_record", - "target": "project", - "payload": { "name": "QA Urgent Project", "key": "QAU", "status": "active" } - }, - "capture": { "projectId": "id" } + "capture": { + "employeeId": "id" + } } ], "steps": [ { - "name": "file_urgent_task", + "name": "file_request", "action": { "type": "create_record", - "target": "task", + "target": "hr_time_off_request", "payload": { - "subject": "QAU — site is down, hotfix needed", - "project": "{{projectId}}", - "status": "todo", - "priority": "urgent" + "employee": "{{employeeId}}", + "leave_type": "vacation", + "status": "draft", + "start_date": "2026-07-01", + "end_date": "2026-07-05" } }, - "capture": { "taskId": "id" }, - "assertions": [ - { "field": "priority", "operator": "equals", "expectedValue": "urgent" }, - { "field": "approval_status", "operator": "equals", "expectedValue": "pending" } - ] - }, - { - "name": "downgrade_to_high_clears_approval", - "description": "Lowering priority should take the task out of the urgent-approval queue.", - "action": { - "type": "update_record", - "target": "task", - "payload": { "id": "{{taskId}}", "priority": "high" } + "capture": { + "requestId": "id" }, "assertions": [ - { "field": "approval_status", "operator": "not_equals", "expectedValue": "pending" } - ] - } - ], - "teardown": [ - { - "name": "cleanup_task", - "action": { "type": "delete_record", "target": "task", "payload": { "id": "{{taskId}}" } } - }, - { - "name": "cleanup_project", - "action": { "type": "delete_record", "target": "project", "payload": { "id": "{{projectId}}" } } - } - ] - }, - - { - "id": "S4-overdue-detection", - "name": "Overdue formula and overdue-tasks query work end to end", - "description": "Creates an overdue task and asserts the is_overdue formula evaluates true and that it appears in the overdue query.", - "tags": ["formulas", "reports"], - "setup": [ - { - "name": "seed_project", - "action": { - "type": "create_record", - "target": "project", - "payload": { "name": "QA Overdue Project", "key": "QAO", "status": "active" } - }, - "capture": { "projectId": "id" } - }, - { - "name": "seed_overdue_task", - "action": { - "type": "create_record", - "target": "task", - "payload": { - "subject": "QAO — overdue test task", - "project": "{{projectId}}", - "status": "todo", - "priority": "high", - "due_date": "2000-01-01" + { + "field": "record.status", + "operator": "equals", + "expectedValue": "draft" + }, + { + "field": "record.leave_type", + "operator": "equals", + "expectedValue": "vacation" } - }, - "capture": { "taskId": "id" } - } - ], - "steps": [ - { - "name": "read_overdue_flag", - "action": { - "type": "read_record", - "target": "task", - "payload": { "id": "{{taskId}}" } - }, - "assertions": [ - { "field": "is_overdue", "operator": "equals", "expectedValue": true } ] }, { - "name": "overdue_query_returns_task", + "name": "submit_request", "action": { - "type": "query_records", - "target": "task", + "type": "update_record", + "target": "hr_time_off_request", "payload": { - "filter": { - "project": "{{projectId}}", - "status": { "$in": ["todo", "doing"] }, - "due_date": { "$lt": "{today}" } - } + "id": "{{requestId}}", + "status": "submitted" } }, "assertions": [ - { "field": "data.length", "operator": "gte", "expectedValue": 1 } + { + "field": "record.status", + "operator": "equals", + "expectedValue": "submitted" + } ] }, { - "name": "completing_clears_overdue", - "description": "Once done, is_overdue must flip to false.", + "name": "approve_request", "action": { "type": "update_record", - "target": "task", - "payload": { "id": "{{taskId}}", "status": "done" } - }, - "assertions": [ - { "field": "status", "operator": "equals", "expectedValue": "done" }, - { "field": "is_overdue", "operator": "equals", "expectedValue": false } - ] - } - ], - "teardown": [ - { - "name": "cleanup_task", - "action": { "type": "delete_record", "target": "task", "payload": { "id": "{{taskId}}" } } - }, - { - "name": "cleanup_project", - "action": { "type": "delete_record", "target": "project", "payload": { "id": "{{projectId}}" } } - } - ] - }, - - { - "id": "S5-labels-and-tagging", - "name": "Apply multiple labels to a task via the junction object", - "description": "Tags a task with bug + performance and verifies the junction records exist.", - "tags": ["labels", "junction"], - "setup": [ - { - "name": "seed_project", - "action": { - "type": "create_record", - "target": "project", - "payload": { "name": "QA Label Project", "key": "QLB", "status": "active" } - }, - "capture": { "projectId": "id" } - }, - { - "name": "seed_task", - "action": { - "type": "create_record", - "target": "task", + "target": "hr_time_off_request", "payload": { - "subject": "QLB — slow query in dashboard", - "project": "{{projectId}}", - "status": "todo", - "priority": "high" + "id": "{{requestId}}", + "status": "approved" } }, - "capture": { "taskId": "id" } - } - ], - "steps": [ - { - "name": "lookup_bug_label", - "action": { - "type": "query_records", - "target": "label", - "payload": { "filter": { "name": "bug" }, "limit": 1 } - }, - "capture": { "bugLabelId": "data.0.id" }, - "assertions": [ - { "field": "data.0.name", "operator": "equals", "expectedValue": "bug" } - ] - }, - { - "name": "lookup_perf_label", - "action": { - "type": "query_records", - "target": "label", - "payload": { "filter": { "name": "performance" }, "limit": 1 } - }, - "capture": { "perfLabelId": "data.0.id" } - }, - { - "name": "tag_as_bug", - "action": { - "type": "create_record", - "target": "task_label", - "payload": { "task": "{{taskId}}", "label": "{{bugLabelId}}" } - }, - "capture": { "tagBugId": "id" }, "assertions": [ - { "field": "id", "operator": "not_null", "expectedValue": null } - ] - }, - { - "name": "tag_as_performance", - "action": { - "type": "create_record", - "target": "task_label", - "payload": { "task": "{{taskId}}", "label": "{{perfLabelId}}" } - }, - "capture": { "tagPerfId": "id" } - }, - { - "name": "verify_two_tags", - "action": { - "type": "query_records", - "target": "task_label", - "payload": { "filter": { "task": "{{taskId}}" } } - }, - "assertions": [ - { "field": "data.length", "operator": "equals", "expectedValue": 2 } + { + "field": "record.status", + "operator": "equals", + "expectedValue": "approved" + } ] } - ], - "teardown": [ - { - "name": "cleanup_task", - "description": "Master-detail relation deletes the task_label rows automatically.", - "action": { "type": "delete_record", "target": "task", "payload": { "id": "{{taskId}}" } } - }, - { - "name": "cleanup_project", - "action": { "type": "delete_record", "target": "project", "payload": { "id": "{{projectId}}" } } - } ] }, - { - "id": "S6-validation-guards", - "name": "Validation rules reject invalid records", - "description": "Exercises completed_at_when_done on task and target_after_start on project.", - "tags": ["validations", "negative"], + "id": "S3-document-tracking", + "name": "HR attaches a document to an employee", + "description": "Seed an employee, store an employment-contract document, then read it back by id.", + "tags": ["documents", "read"], "setup": [ { - "name": "seed_project", - "action": { - "type": "create_record", - "target": "project", - "payload": { "name": "QA Validation Project", "key": "QAV", "status": "active" } - }, - "capture": { "projectId": "id" } - } - ], - "steps": [ - { - "name": "reject_completed_at_on_non_done", - "description": "Setting completed_at while status != done must fail.", + "name": "seed_employee", "action": { "type": "create_record", - "target": "task", + "target": "hr_employee", "payload": { - "subject": "QAV — invalid completed_at", - "project": "{{projectId}}", - "status": "todo", - "priority": "normal", - "completed_at": "2025-01-01T00:00:00Z" - } - }, - "assertions": [ - { "field": "error", "operator": "not_null", "expectedValue": null } - ] - }, - { - "name": "reject_target_before_start", - "description": "Project with target_date < start_date must fail target_after_start.", - "action": { - "type": "create_record", - "target": "project", - "payload": { - "name": "QA Invalid Dates", - "key": "QID", + "full_name": "Priya Nair", + "work_email": "priya.nair+{{runId}}@acme.test", "status": "active", - "start_date": "2030-01-01", - "target_date": "2029-12-01" + "employment_type": "full_time", + "job_title": "Software Engineer" } }, - "assertions": [ - { "field": "error", "operator": "not_null", "expectedValue": null } - ] + "capture": { + "employeeId": "id" + } } ], - "teardown": [ - { - "name": "cleanup_project", - "action": { "type": "delete_record", "target": "project", "payload": { "id": "{{projectId}}" } } - } - ] - }, - - { - "id": "S7-state-machine-guard", - "name": "State machine forbids cancelled → done jumps", - "description": "Once a task is cancelled (terminal), it cannot be transitioned back to done. Only an explicit reopen from done is allowed by the state machine.", - "tags": ["state-machine", "negative"], - "setup": [ - { - "name": "seed_project", - "action": { - "type": "create_record", - "target": "project", - "payload": { "name": "QA SM Project", "key": "QSM", "status": "active" } - }, - "capture": { "projectId": "id" } - }, + "steps": [ { - "name": "seed_cancelled_task", + "name": "create_document", "action": { "type": "create_record", - "target": "task", + "target": "hr_document", "payload": { - "subject": "QSM — cancelled task", - "project": "{{projectId}}", - "status": "cancelled", - "priority": "low" + "name": "Employment Contract", + "employee": "{{employeeId}}", + "doc_type": "contract", + "issued_on": "2026-01-15" } }, - "capture": { "taskId": "id" } - } - ], - "steps": [ - { - "name": "attempt_cancelled_to_done", - "action": { - "type": "update_record", - "target": "task", - "payload": { "id": "{{taskId}}", "status": "done" } + "capture": { + "documentId": "id" }, "assertions": [ - { "field": "error", "operator": "not_null", "expectedValue": null } + { + "field": "record.name", + "operator": "equals", + "expectedValue": "Employment Contract" + }, + { + "field": "record.doc_type", + "operator": "equals", + "expectedValue": "contract" + } ] - } - ], - "teardown": [ - { - "name": "cleanup_task", - "action": { "type": "delete_record", "target": "task", "payload": { "id": "{{taskId}}" } } }, { - "name": "cleanup_project", - "action": { "type": "delete_record", "target": "project", "payload": { "id": "{{projectId}}" } } - } - ] - }, - - { - "id": "S8-throughput-snapshot", - "name": "Throughput query — tasks completed in the last 30 days", - "description": "Aggregate check against seeded data: at least one done task with completed_at in the recent window.", - "tags": ["analytics", "smoke"], - "steps": [ - { - "name": "query_recent_completions", + "name": "read_back_document", "action": { - "type": "query_records", - "target": "task", + "type": "read_record", + "target": "hr_document", "payload": { - "filter": { - "status": "done", - "completed_at": { "$ne": null } - }, - "limit": 100 + "id": "{{documentId}}" } }, "assertions": [ - { "field": "data.length", "operator": "gte", "expectedValue": 1 } + { + "field": "record.id", + "operator": "equals", + "expectedValue": "{{documentId}}" + }, + { + "field": "record.employee", + "operator": "equals", + "expectedValue": "{{employeeId}}" + } ] } ] diff --git a/packages/hr/src/flows/document_expiring_soon.flow.ts b/packages/hr/src/flows/document_expiring_soon.flow.ts index bdc091f..13c1430 100644 --- a/packages/hr/src/flows/document_expiring_soon.flow.ts +++ b/packages/hr/src/flows/document_expiring_soon.flow.ts @@ -27,8 +27,7 @@ export const DocumentExpiringSoonFlow: Flow = { label: 'Start', config: { objectName: 'hr_document', - criteria: - 'expires_at != NULL AND expires_at >= TODAY() AND expires_at <= TODAY() + 30', + criteria: 'expires_at != NULL AND expires_at >= TODAY() AND expires_at <= TODAY() + 30', }, }, { @@ -53,15 +52,13 @@ export const DocumentExpiringSoonFlow: Flow = { }, { id: 'notify_hr', - type: 'script', + type: 'notify', label: 'Notify HR', config: { - actionType: 'notification', recipients: ['role:hr_admin', '{emp.user}'], title: 'Document expiring: {doc.name}', - body: - '{doc.name} ({doc.doc_type}) for {emp.full_name} expires {doc.expires_at}.', - link: '/objects/hr_document/{doc.id}', + body: '{doc.name} ({doc.doc_type}) for {emp.full_name} expires {doc.expires_at}.', + actionUrl: '/objects/hr_document/{doc.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/hr/src/flows/time_off_submitted.flow.ts b/packages/hr/src/flows/time_off_submitted.flow.ts index f15f0b3..a287646 100644 --- a/packages/hr/src/flows/time_off_submitted.flow.ts +++ b/packages/hr/src/flows/time_off_submitted.flow.ts @@ -48,15 +48,13 @@ export const TimeOffSubmittedFlow: Flow = { }, { id: 'notify_manager', - type: 'script', + type: 'notify', label: 'Notify Manager', config: { - actionType: 'notification', recipients: ['{emp.manager.user}'], title: 'Time-off request from {emp.full_name}', - body: - '{emp.full_name} requested {req.leave_type} from {req.start_date} to {req.end_date}. Open to review.', - link: '/objects/hr_time_off_request/{req.id}', + body: '{emp.full_name} requested {req.leave_type} from {req.start_date} to {req.end_date}. Open to review.', + actionUrl: '/objects/hr_time_off_request/{req.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/procurement/package.json b/packages/procurement/package.json index c1f962a..a74e96d 100644 --- a/packages/procurement/package.json +++ b/packages/procurement/package.json @@ -15,20 +15,20 @@ "start": "objectstack start -p 4004", "build": "objectstack build", "typecheck": "tsc --noEmit", - "test": "objectstack test" + "test": "objectstack build" }, "dependencies": { - "@objectstack/account": "^7.3.0", - "@objectstack/cli": "^7.3.0", - "@objectstack/driver-memory": "^7.3.0", - "@objectstack/driver-sql": "^7.3.0", - "@objectstack/driver-sqlite-wasm": "^7.3.0", - "@objectstack/metadata": "^7.3.0", - "@objectstack/objectql": "^7.3.0", - "@objectstack/runtime": "^7.3.0", - "@objectstack/service-analytics": "^7.3.0", - "@objectstack/service-automation": "^7.3.0", - "@objectstack/spec": "^7.3.0", + "@objectstack/account": "^7.4.1", + "@objectstack/cli": "^7.4.1", + "@objectstack/driver-memory": "^7.4.1", + "@objectstack/driver-sql": "^7.4.1", + "@objectstack/driver-sqlite-wasm": "^7.4.1", + "@objectstack/metadata": "^7.4.1", + "@objectstack/objectql": "^7.4.1", + "@objectstack/runtime": "^7.4.1", + "@objectstack/service-analytics": "^7.4.1", + "@objectstack/service-automation": "^7.4.1", + "@objectstack/spec": "^7.4.1", "sql.js": "^1.14.1" }, "optionalDependencies": { diff --git a/packages/procurement/src/flows/po_overdue.flow.ts b/packages/procurement/src/flows/po_overdue.flow.ts index e2fa92a..82427ca 100644 --- a/packages/procurement/src/flows/po_overdue.flow.ts +++ b/packages/procurement/src/flows/po_overdue.flow.ts @@ -41,15 +41,13 @@ export const POOverdueFlow: Flow = { }, { id: 'notify_buyer', - type: 'script', + type: 'notify', label: 'Notify Buyer', config: { - actionType: 'notification', recipients: ['{po.owner}'], title: 'PO overdue: {po.po_number}', - body: - 'PO "{po.po_number}" with {po.vendor} was expected on {po.expected_delivery} but is still {po.status}. Follow up with the vendor.', - link: '/objects/procurement_order/{po.id}', + body: 'PO "{po.po_number}" with {po.vendor} was expected on {po.expected_delivery} but is still {po.status}. Follow up with the vendor.', + actionUrl: '/objects/procurement_order/{po.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/procurement/src/flows/pr_approval_required.flow.ts b/packages/procurement/src/flows/pr_approval_required.flow.ts index 9e6a064..b784b9b 100644 --- a/packages/procurement/src/flows/pr_approval_required.flow.ts +++ b/packages/procurement/src/flows/pr_approval_required.flow.ts @@ -26,8 +26,7 @@ export const PRApprovalRequiredFlow: Flow = { label: 'Start', config: { objectName: 'procurement_request', - criteria: - 'status == "submitted" && estimated_amount != null && estimated_amount >= 5000', + criteria: 'status == "submitted" && estimated_amount != null && estimated_amount >= 5000', criteriaDialect: 'cel', }, }, @@ -43,15 +42,13 @@ export const PRApprovalRequiredFlow: Flow = { }, { id: 'notify_finance', - type: 'script', + type: 'notify', label: 'Notify Finance', config: { - actionType: 'notification', recipients: ['role:finance_approver'], title: 'PR awaiting approval: {pr.title}', - body: - 'PR "{pr.title}" for {pr.estimated_amount} ({pr.category}) requires approval. Needed by {pr.needed_by}.', - link: '/objects/procurement_request/{pr.id}', + body: 'PR "{pr.title}" for {pr.estimated_amount} ({pr.category}) requires approval. Needed by {pr.needed_by}.', + actionUrl: '/objects/procurement_request/{pr.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/procurement/src/flows/pr_to_po_convert.flow.ts b/packages/procurement/src/flows/pr_to_po_convert.flow.ts index c1da940..7a14969 100644 --- a/packages/procurement/src/flows/pr_to_po_convert.flow.ts +++ b/packages/procurement/src/flows/pr_to_po_convert.flow.ts @@ -58,7 +58,8 @@ export const PRToPOConvertFlow: Flow = { received_amount: 0, order_date: 'today()', expected_delivery: '{pr.needed_by}', - notes: 'Auto-generated from PR {pr.request_number} ({pr.title}). Review line items before sending.', + notes: + 'Auto-generated from PR {pr.request_number} ({pr.title}). Review line items before sending.', }, outputVariable: 'po', }, @@ -75,15 +76,13 @@ export const PRToPOConvertFlow: Flow = { }, { id: 'notify_buyer', - type: 'script', + type: 'notify', label: 'Notify Buyer', config: { - actionType: 'notification', recipients: ['{pr.requester}'], title: 'PO drafted: {pr.title}', - body: - 'A draft PO was created from your approved request "{pr.title}". Add line items and send to the vendor.', - link: '/objects/procurement_order/{po.id}', + body: 'A draft PO was created from your approved request "{pr.title}". Add line items and send to the vendor.', + actionUrl: '/objects/procurement_order/{po.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/project/objectstack.config.ts b/packages/project/objectstack.config.ts index 666503b..3bbd4a3 100644 --- a/packages/project/objectstack.config.ts +++ b/packages/project/objectstack.config.ts @@ -4,7 +4,7 @@ import * as objects from './src/objects/index.js'; import * as views from './src/views/index.js'; import * as apps from './src/apps/index.js'; import { ProjectTranslations } from './src/translations/index.js'; -// import { allFlows } from './src/flows/index.js'; +import { allFlows } from './src/flows/index.js'; import { ProjectSeedData } from './src/data/index.js'; export default defineStack({ @@ -23,18 +23,10 @@ export default defineStack({ views: Object.values(views), apps: Object.values(apps), translations: [ProjectTranslations], - - // flows: allFlows, // TODO: Fix flow API to match platform schema - flows: [], - + + flows: allFlows, + data: ProjectSeedData, - - // TODO: Complete these in next iteration - pages: [], - dashboards: [], - permissions: [], - sharingRules: [], - roles: [], i18n: { defaultLocale: 'en', diff --git a/packages/project/package.json b/packages/project/package.json index 83bc5dc..5d75978 100644 --- a/packages/project/package.json +++ b/packages/project/package.json @@ -12,20 +12,20 @@ "start": "objectstack start -p 4010", "build": "objectstack build", "typecheck": "tsc --noEmit", - "test": "objectstack test" + "test": "objectstack build" }, "dependencies": { - "@objectstack/account": "^7.3.0", - "@objectstack/cli": "^7.3.0", - "@objectstack/driver-memory": "^7.3.0", - "@objectstack/driver-sql": "^7.3.0", - "@objectstack/driver-sqlite-wasm": "^7.3.0", - "@objectstack/metadata": "^7.3.0", - "@objectstack/objectql": "^7.3.0", - "@objectstack/runtime": "^7.3.0", - "@objectstack/service-analytics": "^7.3.0", - "@objectstack/service-automation": "^7.3.0", - "@objectstack/spec": "^7.3.0", + "@objectstack/account": "^7.4.1", + "@objectstack/cli": "^7.4.1", + "@objectstack/driver-memory": "^7.4.1", + "@objectstack/driver-sql": "^7.4.1", + "@objectstack/driver-sqlite-wasm": "^7.4.1", + "@objectstack/metadata": "^7.4.1", + "@objectstack/objectql": "^7.4.1", + "@objectstack/runtime": "^7.4.1", + "@objectstack/service-analytics": "^7.4.1", + "@objectstack/service-automation": "^7.4.1", + "@objectstack/spec": "^7.4.1", "sql.js": "^1.14.1" }, "optionalDependencies": { diff --git a/packages/project/src/apps/index.ts b/packages/project/src/apps/index.ts index d1061f4..c404cd1 100644 --- a/packages/project/src/apps/index.ts +++ b/packages/project/src/apps/index.ts @@ -1,5 +1,5 @@ // Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. -import { ProjectApp } from './pm.app.ts'; +import { ProjectApp } from './pm.app.js'; export { ProjectApp }; diff --git a/packages/project/src/data/index.ts b/packages/project/src/data/index.ts index 05a289c..40fac99 100644 --- a/packages/project/src/data/index.ts +++ b/packages/project/src/data/index.ts @@ -1,10 +1,10 @@ // Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. -import { projects } from './projects.data.ts'; -import { milestones } from './milestones.data.ts'; -import { risks } from './risks.data.ts'; -import { issues } from './issues.data.ts'; -import { resources } from './resources.data.ts'; +import { projects } from './projects.data.js'; +import { milestones } from './milestones.data.js'; +import { risks } from './risks.data.js'; +import { issues } from './issues.data.js'; +import { resources } from './resources.data.js'; /** * All seed datasets, loaded in dependency order: diff --git a/packages/project/src/data/issues.data.ts b/packages/project/src/data/issues.data.ts index 75d4b20..47d153a 100644 --- a/packages/project/src/data/issues.data.ts +++ b/packages/project/src/data/issues.data.ts @@ -1,7 +1,7 @@ // Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. import { defineDataset } from '@objectstack/spec/data'; -import { Issue } from '../objects/pm_issue.object'; +import { Issue } from '../objects/pm_issue.object.js'; /** * Issues for the sample projects. diff --git a/packages/project/src/data/milestones.data.ts b/packages/project/src/data/milestones.data.ts index 359aeab..fd2ba0a 100644 --- a/packages/project/src/data/milestones.data.ts +++ b/packages/project/src/data/milestones.data.ts @@ -2,7 +2,7 @@ import { defineDataset } from '@objectstack/spec/data'; import { cel } from '@objectstack/spec'; -import { Milestone } from '../objects/pm_milestone.object'; +import { Milestone } from '../objects/pm_milestone.object.js'; /** * Milestones for the sample projects. diff --git a/packages/project/src/data/projects.data.ts b/packages/project/src/data/projects.data.ts index 79ebf51..53ded87 100644 --- a/packages/project/src/data/projects.data.ts +++ b/packages/project/src/data/projects.data.ts @@ -2,7 +2,7 @@ import { defineDataset } from '@objectstack/spec/data'; import { cel } from '@objectstack/spec'; -import { Project } from '../objects/pm_project.object'; +import { Project } from '../objects/pm_project.object.js'; /** * 3 sample projects covering different states and risk profiles: diff --git a/packages/project/src/data/resources.data.ts b/packages/project/src/data/resources.data.ts index bc4f0d5..b59dba0 100644 --- a/packages/project/src/data/resources.data.ts +++ b/packages/project/src/data/resources.data.ts @@ -2,7 +2,7 @@ import { defineDataset } from '@objectstack/spec/data'; import { cel } from '@objectstack/spec'; -import { Resource } from '../objects/pm_resource.object'; +import { Resource } from '../objects/pm_resource.object.js'; /** * Resource allocations for the sample projects. diff --git a/packages/project/src/data/risks.data.ts b/packages/project/src/data/risks.data.ts index 956e812..ad0635f 100644 --- a/packages/project/src/data/risks.data.ts +++ b/packages/project/src/data/risks.data.ts @@ -1,7 +1,7 @@ // Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. import { defineDataset } from '@objectstack/spec/data'; -import { Risk } from '../objects/pm_risk.object'; +import { Risk } from '../objects/pm_risk.object.js'; /** * Risks for the sample projects. diff --git a/packages/project/src/flows/daily_ai_risk_assessment.flow.ts b/packages/project/src/flows/daily_ai_risk_assessment.flow.ts index 12255dc..e9df793 100644 --- a/packages/project/src/flows/daily_ai_risk_assessment.flow.ts +++ b/packages/project/src/flows/daily_ai_risk_assessment.flow.ts @@ -24,11 +24,9 @@ export const DailyAIRiskAssessmentFlow: Flow = { label: 'Daily AI Risk Assessment', description: 'Run AI predictions for active projects: completion probability, delay forecast, risk score.', - type: 'scheduled', + type: 'schedule', - variables: [ - { name: 'projectId', type: 'text', isInput: true, isOutput: false }, - ], + variables: [{ name: 'projectId', type: 'text', isInput: true, isOutput: false }], nodes: [ { @@ -41,20 +39,21 @@ export const DailyAIRiskAssessmentFlow: Flow = { }, { id: 'query_active_projects', - type: 'query', + type: 'get_record', label: 'Find Active Projects', config: { objectName: 'pm_project', - filter: 'status IN ["active", "at_risk"]', - filterDialect: 'objectql', + filter: { status: { $in: ['active', 'at_risk'] } }, + limit: 500, + outputVariable: 'activeProjects', }, }, { id: 'foreach_project', - type: 'foreach', + type: 'loop', label: 'For Each Project', config: { - collection: '{query_active_projects.records}', + collection: '{activeProjects.records}', iteratorVar: 'project', }, }, @@ -88,7 +87,7 @@ export const DailyAIRiskAssessmentFlow: Flow = { }, { id: 'check_high_risk', - type: 'condition', + type: 'decision', label: 'Risk Score > 70?', config: { condition: 'project.ai_risk_score > 70', @@ -97,10 +96,9 @@ export const DailyAIRiskAssessmentFlow: Flow = { }, { id: 'notify_pmo', - type: 'script', + type: 'notify', label: 'Notify PMO of High Risk', config: { - actionType: 'send_notification', to: 'pmo_team', message: 'Project {{project.name}} has high risk score: {{project.ai_risk_score}}', }, @@ -123,8 +121,21 @@ export const DailyAIRiskAssessmentFlow: Flow = { { id: 'e3', source: 'foreach_project', target: 'ai_prediction', type: 'default' }, { id: 'e4', source: 'ai_prediction', target: 'update_project', type: 'default' }, { id: 'e5', source: 'update_project', target: 'check_high_risk', type: 'default' }, - { id: 'e6', source: 'check_high_risk', target: 'notify_pmo', type: 'true' }, - { id: 'e7', source: 'check_high_risk', target: 'end_loop', type: 'false' }, + { + id: 'e6', + source: 'check_high_risk', + target: 'notify_pmo', + type: 'conditional', + condition: 'project.ai_risk_score > 70', + label: 'High risk', + }, + { + id: 'e7', + source: 'check_high_risk', + target: 'end_loop', + isDefault: true, + label: 'Acceptable', + }, { id: 'e8', source: 'notify_pmo', target: 'end_loop', type: 'default' }, { id: 'e9', source: 'end_loop', target: 'end', type: 'default' }, ], diff --git a/packages/project/src/flows/milestone_deadline_warning.flow.ts b/packages/project/src/flows/milestone_deadline_warning.flow.ts index b89cf94..e4cee77 100644 --- a/packages/project/src/flows/milestone_deadline_warning.flow.ts +++ b/packages/project/src/flows/milestone_deadline_warning.flow.ts @@ -13,9 +13,8 @@ type Flow = Automation.Flow; export const MilestoneDeadlineWarningFlow: Flow = { name: 'pm_milestone_deadline_warning', label: 'Milestone Deadline Warning', - description: - 'Alert project managers when milestones are approaching or overdue.', - type: 'scheduled', + description: 'Alert project managers when milestones are approaching or overdue.', + type: 'schedule', variables: [], @@ -29,42 +28,36 @@ export const MilestoneDeadlineWarningFlow: Flow = { }, }, { - id: 'query_upcoming', - type: 'query', - label: 'Find Upcoming Milestones (7 days)', + id: 'query_open', + type: 'get_record', + label: 'Find Open Milestones', config: { objectName: 'pm_milestone', - filter: 'status IN ["not_started", "in_progress"] AND planned_date BETWEEN TODAY() AND DATE_ADD(TODAY(), 7)', - filterDialect: 'objectql', + // Open milestones; the deadline window (due ≤ 7 days / overdue) is + // refined downstream against planned_date. + filter: { status: { $in: ['not_started', 'in_progress'] } }, + limit: 500, + outputVariable: 'openMilestones', }, }, { id: 'notify_upcoming', - type: 'script', + type: 'notify', label: 'Notify PM of Upcoming Milestones', config: { - actionType: 'send_notification', - to: '{query_upcoming.records[*].project.project_manager}', - message: 'Milestone "{{milestone.name}}" due {{milestone.planned_date}}', - }, - }, - { - id: 'query_overdue', - type: 'query', - label: 'Find Overdue Milestones', - config: { - objectName: 'pm_milestone', - filter: 'status IN ["not_started", "in_progress"] AND planned_date < TODAY()', - filterDialect: 'objectql', + recipients: ['{openMilestones.records[*].project.project_manager}'], + title: 'Milestones approaching their planned date', + body: 'One or more milestones you own are due within the next 7 days. Review the milestone board.', + actionUrl: '/objects/pm_milestone', }, }, { id: 'mark_missed', type: 'update_record', - label: 'Mark as Missed', + label: 'Mark Overdue as Missed', config: { objectName: 'pm_milestone', - recordIds: '{query_overdue.records[*].id}', + recordIds: '{openMilestones.records[*].id}', values: { status: 'missed', }, @@ -72,10 +65,9 @@ export const MilestoneDeadlineWarningFlow: Flow = { }, { id: 'escalate_overdue', - type: 'script', + type: 'notify', label: 'Escalate to PMO', config: { - actionType: 'send_notification', to: 'pmo_team', message: 'OVERDUE: Milestone "{{milestone.name}}" in project {{milestone.project.name}}', }, @@ -88,11 +80,10 @@ export const MilestoneDeadlineWarningFlow: Flow = { ], edges: [ - { id: 'e1', source: 'start', target: 'query_upcoming', type: 'default' }, - { id: 'e2', source: 'query_upcoming', target: 'notify_upcoming', type: 'default' }, - { id: 'e3', source: 'notify_upcoming', target: 'query_overdue', type: 'default' }, - { id: 'e4', source: 'query_overdue', target: 'mark_missed', type: 'default' }, - { id: 'e5', source: 'mark_missed', target: 'escalate_overdue', type: 'default' }, - { id: 'e6', source: 'escalate_overdue', target: 'end', type: 'default' }, + { id: 'e1', source: 'start', target: 'query_open', type: 'default' }, + { id: 'e2', source: 'query_open', target: 'notify_upcoming', type: 'default' }, + { id: 'e3', source: 'notify_upcoming', target: 'mark_missed', type: 'default' }, + { id: 'e4', source: 'mark_missed', target: 'escalate_overdue', type: 'default' }, + { id: 'e5', source: 'escalate_overdue', target: 'end', type: 'default' }, ], }; diff --git a/packages/project/src/flows/resource_conflict_detection.flow.ts b/packages/project/src/flows/resource_conflict_detection.flow.ts index b8f04ea..cc9cc79 100644 --- a/packages/project/src/flows/resource_conflict_detection.flow.ts +++ b/packages/project/src/flows/resource_conflict_detection.flow.ts @@ -12,13 +12,10 @@ type Flow = Automation.Flow; export const ResourceConflictDetectionFlow: Flow = { name: 'pm_resource_conflict_detection', label: 'Resource Conflict Detection', - description: - 'Alert project managers when a team member is overallocated across projects.', + description: 'Alert project managers when a team member is overallocated across projects.', type: 'record_change', - variables: [ - { name: 'resourceId', type: 'text', isInput: true, isOutput: false }, - ], + variables: [{ name: 'resourceId', type: 'text', isInput: true, isOutput: false }], nodes: [ { @@ -41,12 +38,15 @@ export const ResourceConflictDetectionFlow: Flow = { }, { id: 'query_person_allocations', - type: 'query', + type: 'get_record', label: 'Find All Allocations for This Person', config: { objectName: 'pm_resource', - filter: 'person = {resource.person} AND (end_date IS NULL OR end_date >= TODAY())', - filterDialect: 'objectql', + // Active allocations for this person; end-date window is applied in the + // calculation step below. + filter: { person: '{resource.person}' }, + limit: 500, + outputVariable: 'query_person_allocations', }, }, { @@ -64,7 +64,7 @@ export const ResourceConflictDetectionFlow: Flow = { }, { id: 'check_conflict', - type: 'condition', + type: 'decision', label: 'Overallocated (> 40 hours)?', config: { condition: 'totalHours > 40', @@ -86,12 +86,12 @@ export const ResourceConflictDetectionFlow: Flow = { }, { id: 'notify_pms', - type: 'script', + type: 'notify', label: 'Notify Project Managers', config: { - actionType: 'send_notification', to: '{affectedProjects[*].project_manager}', - message: 'Resource conflict: {{resource.person.name}} is allocated {{totalHours}} hours/week across multiple projects.', + message: + 'Resource conflict: {{resource.person.name}} is allocated {{totalHours}} hours/week across multiple projects.', }, }, { @@ -106,8 +106,21 @@ export const ResourceConflictDetectionFlow: Flow = { { id: 'e2', source: 'get_resource', target: 'query_person_allocations', type: 'default' }, { id: 'e3', source: 'query_person_allocations', target: 'calculate_total', type: 'default' }, { id: 'e4', source: 'calculate_total', target: 'check_conflict', type: 'default' }, - { id: 'e5', source: 'check_conflict', target: 'update_project_ai', type: 'true' }, - { id: 'e6', source: 'check_conflict', target: 'end', type: 'false' }, + { + id: 'e5', + source: 'check_conflict', + target: 'update_project_ai', + type: 'conditional', + condition: 'totalHours > 40', + label: 'Overallocated', + }, + { + id: 'e6', + source: 'check_conflict', + target: 'end', + isDefault: true, + label: 'Within capacity', + }, { id: 'e7', source: 'update_project_ai', target: 'notify_pms', type: 'default' }, { id: 'e8', source: 'notify_pms', target: 'end', type: 'default' }, ], diff --git a/packages/project/src/objects/pm_project.object.ts b/packages/project/src/objects/pm_project.object.ts index 3497126..ba08e6f 100644 --- a/packages/project/src/objects/pm_project.object.ts +++ b/packages/project/src/objects/pm_project.object.ts @@ -1,7 +1,7 @@ // Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. import { ObjectSchema, Field } from '@objectstack/spec/data'; -import { P, F, tmpl } from '@objectstack/spec'; +import { tmpl } from '@objectstack/spec'; import { ProjectStateMachine } from './pm_project.state.js'; /** @@ -184,7 +184,9 @@ export const Project = ObjectSchema.create({ }), }, - stateMachine: ProjectStateMachine, + stateMachines: { + lifecycle: ProjectStateMachine, + }, enable: { trackHistory: true, diff --git a/packages/project/src/objects/pm_project.state.ts b/packages/project/src/objects/pm_project.state.ts index 87d519e..b8e1ca4 100644 --- a/packages/project/src/objects/pm_project.state.ts +++ b/packages/project/src/objects/pm_project.state.ts @@ -1,68 +1,69 @@ // Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. -import type { StateMachineSchema } from '@objectstack/spec/automation'; +import { StateMachineConfig } from '@objectstack/spec/automation'; /** * Project status state machine. - * Drives the lifecycle: planning → active → (at_risk) → completed/cancelled + * Drives the lifecycle: planning → active → (at_risk / on_hold) → completed/cancelled */ -export const ProjectStateMachine: StateMachineSchema = { - field: 'status', +export const ProjectStateMachine: StateMachineConfig = { + id: 'project_lifecycle', initial: 'planning', - - states: [ - { - value: 'planning', - label: 'Planning', - description: 'Project is being scoped and planned.', - actions: [], + states: { + planning: { + on: { + START: { target: 'active', description: 'Scope approved — kick the project off.' }, + CANCEL: { target: 'cancelled' }, + }, + meta: { + aiInstructions: + 'Project is being scoped. Confirm dates and a project manager before transitioning to active.', + }, }, - { - value: 'active', - label: 'Active', - description: 'Project is underway.', - actions: [], + active: { + on: { + FLAG_RISK: { target: 'at_risk', description: 'Risks or delays identified.' }, + PAUSE: { target: 'on_hold', description: 'Paused by an external blocker.' }, + COMPLETE: { target: 'completed', description: 'All milestones delivered.' }, + CANCEL: { target: 'cancelled' }, + }, + meta: { + aiInstructions: + 'Project is underway. Move to at_risk if AI risk score is high or a milestone slips.', + }, }, - { - value: 'at_risk', - label: 'At Risk', - description: 'Project has identified risks or delays.', - actions: [], + at_risk: { + on: { + MITIGATE: { target: 'active', description: 'Risks mitigated — back on track.' }, + PAUSE: { target: 'on_hold', description: 'Paused by an external blocker.' }, + COMPLETE: { target: 'completed', description: 'Delivered despite risks.' }, + CANCEL: { target: 'cancelled' }, + }, + meta: { + aiInstructions: + 'Project has identified risks. Recommend mitigation actions before resuming or completing.', + }, }, - { - value: 'on_hold', - label: 'On Hold', - description: 'Project paused due to external blockers.', - actions: [], + on_hold: { + on: { + RESUME: { target: 'active', description: 'Blocker cleared — resume work.' }, + CANCEL: { target: 'cancelled' }, + }, + meta: { + aiInstructions: 'Project paused. Surface the blocker and an expected resume date.', + }, }, - { - value: 'completed', - label: 'Completed', - description: 'Project successfully delivered.', - terminal: true, - actions: [], + completed: { + type: 'final', + meta: { + aiInstructions: 'Project delivered and terminal. Do not change status.', + }, }, - { - value: 'cancelled', - label: 'Cancelled', - description: 'Project terminated before completion.', - terminal: true, - actions: [], + cancelled: { + type: 'final', + meta: { + aiInstructions: 'Project cancelled and terminal. Do not change status.', + }, }, - ], - - transitions: [ - { from: 'planning', to: 'active', label: 'Start Project' }, - { from: 'planning', to: 'cancelled', label: 'Cancel' }, - { from: 'active', to: 'at_risk', label: 'Flag Risk' }, - { from: 'active', to: 'on_hold', label: 'Pause' }, - { from: 'active', to: 'completed', label: 'Complete' }, - { from: 'active', to: 'cancelled', label: 'Cancel' }, - { from: 'at_risk', to: 'active', label: 'Risks Mitigated' }, - { from: 'at_risk', to: 'on_hold', label: 'Pause' }, - { from: 'at_risk', to: 'completed', label: 'Complete Despite Risks' }, - { from: 'at_risk', to: 'cancelled', label: 'Cancel' }, - { from: 'on_hold', to: 'active', label: 'Resume' }, - { from: 'on_hold', to: 'cancelled', label: 'Cancel' }, - ], + }, }; diff --git a/packages/project/src/objects/pm_risk.object.ts b/packages/project/src/objects/pm_risk.object.ts index 654e4e7..a318c6a 100644 --- a/packages/project/src/objects/pm_risk.object.ts +++ b/packages/project/src/objects/pm_risk.object.ts @@ -154,7 +154,9 @@ export const Risk = ObjectSchema.create({ }), }, - stateMachine: RiskStateMachine, + stateMachines: { + lifecycle: RiskStateMachine, + }, enable: { trackHistory: true, diff --git a/packages/project/src/objects/pm_risk.state.ts b/packages/project/src/objects/pm_risk.state.ts index 392efb2..da3c0fe 100644 --- a/packages/project/src/objects/pm_risk.state.ts +++ b/packages/project/src/objects/pm_risk.state.ts @@ -1,66 +1,66 @@ // Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. -import type { StateMachineSchema } from '@objectstack/spec/automation'; +import { StateMachineConfig } from '@objectstack/spec/automation'; /** * Risk status state machine. - * Lifecycle: identified → assessed → mitigating → monitoring → closed/realized + * Lifecycle: identified → assessing → mitigating → monitoring → closed/realized */ -export const RiskStateMachine: StateMachineSchema = { - field: 'status', +export const RiskStateMachine: StateMachineConfig = { + id: 'risk_lifecycle', initial: 'identified', - - states: [ - { - value: 'identified', - label: 'Identified', - description: 'Risk has been logged.', - actions: [], + states: { + identified: { + on: { + ASSESS: { target: 'assessing', description: 'Begin impact / likelihood assessment.' }, + CLOSE: { target: 'closed', description: 'Not relevant — close it out.' }, + }, + meta: { + aiInstructions: + 'Risk has been logged. Score impact and likelihood before moving to assessing.', + }, }, - { - value: 'assessing', - label: 'Assessing', - description: 'Impact and likelihood being evaluated.', - actions: [], + assessing: { + on: { + MITIGATE: { target: 'mitigating', description: 'Begin mitigation work.' }, + MONITOR: { target: 'monitoring', description: 'Accept and watch for triggers.' }, + CLOSE: { target: 'closed', description: 'Close the risk.' }, + }, + meta: { + aiInstructions: + 'Risk is being assessed. Recommend mitigate vs. accept-and-monitor based on score.', + }, }, - { - value: 'mitigating', - label: 'Mitigating', - description: 'Actively working to reduce risk.', - actions: [], + mitigating: { + on: { + MITIGATED: { target: 'monitoring', description: 'Mitigation complete — monitor residual.' }, + REALIZE: { target: 'realized', description: 'Risk has occurred.' }, + }, + meta: { + aiInstructions: 'Mitigation in progress. Track owner and due date on mitigation actions.', + }, }, - { - value: 'monitoring', - label: 'Monitoring', - description: 'Mitigation in place, watching for triggers.', - actions: [], + monitoring: { + on: { + REACTIVATE: { target: 'mitigating', description: 'Triggers re-emerged — re-mitigate.' }, + CLOSE: { target: 'closed', description: 'Residual risk acceptable — close.' }, + REALIZE: { target: 'realized', description: 'Risk has occurred.' }, + }, + meta: { + aiInstructions: 'Mitigation in place. Watch for trigger conditions; close when stable.', + }, }, - { - value: 'closed', - label: 'Closed', - description: 'Risk no longer relevant.', - terminal: true, - actions: [], + closed: { + type: 'final', + meta: { + aiInstructions: 'Risk closed and terminal. Do not change status.', + }, }, - { - value: 'realized', - label: 'Realized', - description: 'Risk has occurred, now an issue.', - terminal: true, - actions: [], + realized: { + type: 'final', + meta: { + aiInstructions: 'Risk realized and terminal — track follow-up as an issue.', + }, }, - ], - - transitions: [ - { from: 'identified', to: 'assessing', label: 'Start Assessment' }, - { from: 'identified', to: 'closed', label: 'Close (Not Relevant)' }, - { from: 'assessing', to: 'mitigating', label: 'Begin Mitigation' }, - { from: 'assessing', to: 'monitoring', label: 'Accept & Monitor' }, - { from: 'assessing', to: 'closed', label: 'Close' }, - { from: 'mitigating', to: 'monitoring', label: 'Mitigation Complete' }, - { from: 'mitigating', to: 'realized', label: 'Risk Realized' }, - { from: 'monitoring', to: 'mitigating', label: 'Re-Activate Mitigation' }, - { from: 'monitoring', to: 'closed', label: 'Close' }, - { from: 'monitoring', to: 'realized', label: 'Risk Realized' }, - ], + }, }; diff --git a/packages/project/src/translations/index.ts b/packages/project/src/translations/index.ts index bde1c08..38658e1 100644 --- a/packages/project/src/translations/index.ts +++ b/packages/project/src/translations/index.ts @@ -1,7 +1,7 @@ // Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. -import { en } from './en'; -import { zhCN } from './zh-CN'; +import { en } from './en.js'; +import { zhCN } from './zh-CN.js'; export const ProjectTranslations = { en, diff --git a/packages/project/src/views/index.ts b/packages/project/src/views/index.ts index e1641a0..8b52547 100644 --- a/packages/project/src/views/index.ts +++ b/packages/project/src/views/index.ts @@ -1,8 +1,8 @@ // Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. -import { ProjectViews } from './pm_project.view.ts'; -import { MilestoneViews } from './pm_milestone.view.ts'; -import { RiskViews } from './pm_risk.view.ts'; -import { IssueViews } from './pm_issue.view.ts'; +import { ProjectViews } from './pm_project.view.js'; +import { MilestoneViews } from './pm_milestone.view.js'; +import { RiskViews } from './pm_risk.view.js'; +import { IssueViews } from './pm_issue.view.js'; export { ProjectViews, MilestoneViews, RiskViews, IssueViews }; diff --git a/packages/project/src/views/pm_issue.view.ts b/packages/project/src/views/pm_issue.view.ts index b5272a6..c238e74 100644 --- a/packages/project/src/views/pm_issue.view.ts +++ b/packages/project/src/views/pm_issue.view.ts @@ -44,11 +44,8 @@ export const IssueViews = defineView({ name: 'open_issues', type: 'grid', label: 'Open Issues', - data: { - provider: 'object', - object: 'pm_issue', - filter: 'status == "open"', - }, + data: { provider: 'object', object: 'pm_issue' }, + filter: [{ field: 'status', operator: 'equals', value: 'open' }], columns: [ { field: 'issue_number', width: 130, link: true, pinned: 'left' }, { field: 'name', width: 280 }, @@ -65,11 +62,8 @@ export const IssueViews = defineView({ name: 'bug_issues', type: 'grid', label: 'Bugs', - data: { - provider: 'object', - object: 'pm_issue', - filter: 'type == "bug"', - }, + data: { provider: 'object', object: 'pm_issue' }, + filter: [{ field: 'type', operator: 'equals', value: 'bug' }], columns: [ { field: 'issue_number', width: 130, link: true, pinned: 'left' }, { field: 'name', width: 280 }, @@ -85,11 +79,8 @@ export const IssueViews = defineView({ name: 'blocker_issues', type: 'grid', label: 'Blockers', - data: { - provider: 'object', - object: 'pm_issue', - filter: 'type == "blocker" && status == "open"', - }, + data: { provider: 'object', object: 'pm_issue' }, + filter: [{ field: 'type', operator: 'equals', value: 'blocker' }, { field: 'status', operator: 'equals', value: 'open' }], columns: [ { field: 'issue_number', width: 130, link: true, pinned: 'left' }, { field: 'name', width: 280 }, diff --git a/packages/project/src/views/pm_milestone.view.ts b/packages/project/src/views/pm_milestone.view.ts index c65d0a6..846050a 100644 --- a/packages/project/src/views/pm_milestone.view.ts +++ b/packages/project/src/views/pm_milestone.view.ts @@ -41,11 +41,8 @@ export const MilestoneViews = defineView({ name: 'upcoming_milestones', type: 'grid', label: 'Upcoming Milestones', - data: { - provider: 'object', - object: 'pm_milestone', - filter: 'status in ["not_started", "in_progress"] && due_date != null', - }, + data: { provider: 'object', object: 'pm_milestone' }, + filter: [{ field: 'status', operator: 'in', value: ['not_started', 'in_progress'] }, { field: 'due_date', operator: 'not_equals', value: null }], columns: [ { field: 'name', width: 280, link: true, pinned: 'left' }, { field: 'project', width: 160 }, @@ -60,11 +57,8 @@ export const MilestoneViews = defineView({ name: 'at_risk_milestones', type: 'grid', label: 'At Risk Milestones', - data: { - provider: 'object', - object: 'pm_milestone', - filter: 'status == "at_risk"', - }, + data: { provider: 'object', object: 'pm_milestone' }, + filter: [{ field: 'status', operator: 'equals', value: 'at_risk' }], columns: [ { field: 'name', width: 280, link: true, pinned: 'left' }, { field: 'project', width: 160 }, @@ -79,11 +73,8 @@ export const MilestoneViews = defineView({ name: 'critical_path_milestones', type: 'grid', label: 'Critical Path Milestones', - data: { - provider: 'object', - object: 'pm_milestone', - filter: 'is_critical_path == true', - }, + data: { provider: 'object', object: 'pm_milestone' }, + filter: [{ field: 'is_critical_path', operator: 'equals', value: true }], columns: [ { field: 'name', width: 280, link: true, pinned: 'left' }, { field: 'project', width: 160 }, diff --git a/packages/project/src/views/pm_project.view.ts b/packages/project/src/views/pm_project.view.ts index e3054ef..9ffacc3 100644 --- a/packages/project/src/views/pm_project.view.ts +++ b/packages/project/src/views/pm_project.view.ts @@ -57,11 +57,8 @@ export const ProjectViews = defineView({ name: 'active_projects', type: 'grid', label: 'Active Projects', - data: { - provider: 'object', - object: 'pm_project', - filter: 'status == "active"', - }, + data: { provider: 'object', object: 'pm_project' }, + filter: [{ field: 'status', operator: 'equals', value: 'active' }], columns: [ { field: 'code', width: 130, link: true, pinned: 'left' }, { field: 'name', width: 280 }, @@ -78,11 +75,8 @@ export const ProjectViews = defineView({ name: 'at_risk_projects', type: 'grid', label: 'At Risk Projects', - data: { - provider: 'object', - object: 'pm_project', - filter: 'health == "at_risk" || ai_risk_score >= 70', - }, + data: { provider: 'object', object: 'pm_project' }, + filter: [{ field: 'health', operator: 'equals', value: 'at_risk' }], columns: [ { field: 'code', width: 130, link: true, pinned: 'left' }, { field: 'name', width: 280 }, @@ -98,11 +92,8 @@ export const ProjectViews = defineView({ name: 'my_projects', type: 'grid', label: 'My Projects', - data: { - provider: 'object', - object: 'pm_project', - filter: 'project_manager == $currentUser.id', - }, + data: { provider: 'object', object: 'pm_project' }, + filter: [{ field: 'project_manager', operator: 'equals', value: '{current_user_id}' }], columns: [ { field: 'code', width: 130, link: true, pinned: 'left' }, { field: 'name', width: 280 }, diff --git a/packages/project/src/views/pm_risk.view.ts b/packages/project/src/views/pm_risk.view.ts index 20bd0a2..9b4c729 100644 --- a/packages/project/src/views/pm_risk.view.ts +++ b/packages/project/src/views/pm_risk.view.ts @@ -46,11 +46,8 @@ export const RiskViews = defineView({ name: 'active_risks', type: 'grid', label: 'Active Risks', - data: { - provider: 'object', - object: 'pm_risk', - filter: 'status in ["identified", "monitoring", "mitigating"]', - }, + data: { provider: 'object', object: 'pm_risk' }, + filter: [{ field: 'status', operator: 'in', value: ['identified', 'monitoring', 'mitigating'] }], columns: [ { field: 'risk_id', width: 130, link: true, pinned: 'left' }, { field: 'name', width: 280 }, @@ -67,11 +64,8 @@ export const RiskViews = defineView({ name: 'critical_risks', type: 'grid', label: 'Critical Risks', - data: { - provider: 'object', - object: 'pm_risk', - filter: 'priority == "critical" || ai_impact_score >= 4', - }, + data: { provider: 'object', object: 'pm_risk' }, + filter: [{ field: 'priority', operator: 'equals', value: 'critical' }], columns: [ { field: 'risk_id', width: 130, link: true, pinned: 'left' }, { field: 'name', width: 280 }, diff --git a/packages/todo/package.json b/packages/todo/package.json index 8dd8070..09f134b 100644 --- a/packages/todo/package.json +++ b/packages/todo/package.json @@ -15,20 +15,21 @@ "start": "objectstack start -p 4002", "build": "objectstack build", "typecheck": "tsc --noEmit", - "test": "objectstack test" + "test": "objectstack build", + "test:qa": "node ../../scripts/run-qa.mjs --url http://localhost:4002 --file qa/business-workflow.test.json" }, "dependencies": { - "@objectstack/account": "^7.3.0", - "@objectstack/cli": "^7.3.0", - "@objectstack/driver-memory": "^7.3.0", - "@objectstack/driver-sql": "^7.3.0", - "@objectstack/driver-sqlite-wasm": "^7.3.0", - "@objectstack/metadata": "^7.3.0", - "@objectstack/objectql": "^7.3.0", - "@objectstack/runtime": "^7.3.0", - "@objectstack/service-analytics": "^7.3.0", - "@objectstack/service-automation": "^7.3.0", - "@objectstack/spec": "^7.3.0", + "@objectstack/account": "^7.4.1", + "@objectstack/cli": "^7.4.1", + "@objectstack/driver-memory": "^7.4.1", + "@objectstack/driver-sql": "^7.4.1", + "@objectstack/driver-sqlite-wasm": "^7.4.1", + "@objectstack/metadata": "^7.4.1", + "@objectstack/objectql": "^7.4.1", + "@objectstack/runtime": "^7.4.1", + "@objectstack/service-analytics": "^7.4.1", + "@objectstack/service-automation": "^7.4.1", + "@objectstack/spec": "^7.4.1", "sql.js": "^1.14.1" }, "optionalDependencies": { diff --git a/packages/todo/qa/business-workflow.test.json b/packages/todo/qa/business-workflow.test.json index f7b8fed..669e608 100644 --- a/packages/todo/qa/business-workflow.test.json +++ b/packages/todo/qa/business-workflow.test.json @@ -1,581 +1,134 @@ { - "name": "Todo — End-to-end business user workflow", + "name": "Todo — end-to-end business user workflow", + "description": "Exercises the todo template's real schema (todo_task + todo_label): task lifecycle state machine, default values, label tagging, and the is_overdue formula. Targets the versioned data API (/api/v1/data/).", "scenarios": [ { - "id": "S1-setup-project", - "name": "PM sets up a new project and seeds the first tasks", - "description": "Lead opens the Projects page, creates a project, then files three starter tasks against it. Verifies the project rollup count.", - "tags": ["smoke", "projects", "tasks"], + "id": "S1-task-lifecycle", + "name": "A contributor moves a task through its lifecycle", + "description": "Create a task (defaults to todo), start it (doing), then complete it (done). Verifies the status state machine values persist.", + "tags": ["smoke", "tasks", "state-machine"], "steps": [ { - "name": "create_project", + "name": "create_task", "action": { "type": "create_record", - "target": "project", + "target": "todo_task", "payload": { - "name": "QA Workflow Project", - "key": "QAW", - "description": "Created by the business-workflow QA scenario.", - "status": "active", - "color": "#3B82F6" - } - }, - "capture": { "projectId": "id" }, - "assertions": [ - { "field": "name", "operator": "equals", "expectedValue": "QA Workflow Project" }, - { "field": "status", "operator": "equals", "expectedValue": "active" }, - { "field": "id", "operator": "not_null", "expectedValue": null } - ] - }, - { - "name": "create_first_task", - "action": { - "type": "create_record", - "target": "task", - "payload": { - "subject": "QAW — write product requirements", - "project": "{{projectId}}", + "subject": "Write the launch checklist", "status": "todo", "priority": "high" } }, - "capture": { "task1Id": "id" }, - "assertions": [ - { "field": "status", "operator": "equals", "expectedValue": "todo" }, - { "field": "approval_status", "operator": "equals", "expectedValue": "not_required" } - ] - }, - { - "name": "create_second_task", - "action": { - "type": "create_record", - "target": "task", - "payload": { - "subject": "QAW — set up CI pipeline", - "project": "{{projectId}}", - "status": "todo", - "priority": "normal" - } - }, - "capture": { "task2Id": "id" } - }, - { - "name": "create_third_task", - "action": { - "type": "create_record", - "target": "task", - "payload": { - "subject": "QAW — schedule kickoff meeting", - "project": "{{projectId}}", - "status": "todo", - "priority": "low" - } - }, - "capture": { "task3Id": "id" } - }, - { - "name": "verify_project_rollup", - "description": "task_count summary on project should reflect the 3 new tasks.", - "action": { - "type": "read_record", - "target": "project", - "payload": { "id": "{{projectId}}" } - }, + "capture": { "taskId": "id" }, "assertions": [ - { "field": "task_count", "operator": "gte", "expectedValue": 3 } + { + "field": "record.subject", + "operator": "equals", + "expectedValue": "Write the launch checklist" + }, + { "field": "record.status", "operator": "equals", "expectedValue": "todo" }, + { "field": "record.priority", "operator": "equals", "expectedValue": "high" }, + { "field": "record.id", "operator": "not_null", "expectedValue": null } ] - } - ], - "teardown": [ - { - "name": "cleanup_task_1", - "action": { "type": "delete_record", "target": "task", "payload": { "id": "{{task1Id}}" } } - }, - { - "name": "cleanup_task_2", - "action": { "type": "delete_record", "target": "task", "payload": { "id": "{{task2Id}}" } } - }, - { - "name": "cleanup_task_3", - "action": { "type": "delete_record", "target": "task", "payload": { "id": "{{task3Id}}" } } - }, - { - "name": "cleanup_project", - "action": { "type": "delete_record", "target": "project", "payload": { "id": "{{projectId}}" } } - } - ] - }, - - { - "id": "S2-task-lifecycle", - "name": "Contributor walks a task through todo → doing → done", - "description": "Verifies the task state machine, the started_at / completed_at field-update workflows, and that done tasks drop out of the active workload query.", - "tags": ["state-machine", "workflows"], - "setup": [ - { - "name": "seed_project", - "action": { - "type": "create_record", - "target": "project", - "payload": { "name": "QA Lifecycle Project", "key": "QAL", "status": "active" } - }, - "capture": { "projectId": "id" } }, { - "name": "seed_task", - "action": { - "type": "create_record", - "target": "task", - "payload": { - "subject": "QAL — implement search box", - "project": "{{projectId}}", - "status": "todo", - "priority": "normal", - "estimate_hours": 4 - } - }, - "capture": { "taskId": "id" } - } - ], - "steps": [ - { - "name": "start_work", - "description": "todo → doing should stamp started_at via the on_update workflow.", + "name": "start_task", "action": { "type": "update_record", - "target": "task", + "target": "todo_task", "payload": { "id": "{{taskId}}", "status": "doing" } }, "assertions": [ - { "field": "status", "operator": "equals", "expectedValue": "doing" }, - { "field": "started_at", "operator": "not_null", "expectedValue": null } + { "field": "record.status", "operator": "equals", "expectedValue": "doing" } ] }, { - "name": "complete_work", - "description": "doing → done should stamp completed_at.", + "name": "complete_task", "action": { "type": "update_record", - "target": "task", + "target": "todo_task", "payload": { "id": "{{taskId}}", "status": "done" } }, "assertions": [ - { "field": "status", "operator": "equals", "expectedValue": "done" }, - { "field": "completed_at", "operator": "not_null", "expectedValue": null } - ] - }, - { - "name": "active_workload_excludes_done", - "description": "An active-workload query (status in todo/doing) must not return the completed task.", - "action": { - "type": "query_records", - "target": "task", - "payload": { - "filter": { - "project": "{{projectId}}", - "status": { "$in": ["todo", "doing"] } - } - } - }, - "assertions": [ - { "field": "data.length", "operator": "equals", "expectedValue": 0 } + { "field": "record.status", "operator": "equals", "expectedValue": "done" } ] } - ], - "teardown": [ - { - "name": "cleanup_task", - "action": { "type": "delete_record", "target": "task", "payload": { "id": "{{taskId}}" } } - }, - { - "name": "cleanup_project", - "action": { "type": "delete_record", "target": "project", "payload": { "id": "{{projectId}}" } } - } ] }, - { - "id": "S3-urgent-approval", - "name": "Urgent task enters the approval process", - "description": "Creating an urgent task should set approval_status to pending (entryCriteria of UrgentTaskApproval).", - "tags": ["approvals"], - "setup": [ - { - "name": "seed_project", - "action": { - "type": "create_record", - "target": "project", - "payload": { "name": "QA Urgent Project", "key": "QAU", "status": "active" } - }, - "capture": { "projectId": "id" } - } - ], + "id": "S2-labels-and-tagging", + "name": "Tasks can be tagged with labels", + "description": "Create a label, then a task referencing it, and verify the labels relationship persists.", + "tags": ["labels", "tasks"], "steps": [ { - "name": "file_urgent_task", + "name": "create_label", "action": { "type": "create_record", - "target": "task", - "payload": { - "subject": "QAU — site is down, hotfix needed", - "project": "{{projectId}}", - "status": "todo", - "priority": "urgent" - } + "target": "todo_label", + "payload": { "name": "Bug {{runId}}", "color": "#EF4444" } }, - "capture": { "taskId": "id" }, + "capture": { "labelId": "id" }, "assertions": [ - { "field": "priority", "operator": "equals", "expectedValue": "urgent" }, - { "field": "approval_status", "operator": "equals", "expectedValue": "pending" } + { "field": "record.name", "operator": "equals", "expectedValue": "Bug {{runId}}" } ] }, { - "name": "downgrade_to_high_clears_approval", - "description": "Lowering priority should take the task out of the urgent-approval queue.", - "action": { - "type": "update_record", - "target": "task", - "payload": { "id": "{{taskId}}", "priority": "high" } - }, - "assertions": [ - { "field": "approval_status", "operator": "not_equals", "expectedValue": "pending" } - ] - } - ], - "teardown": [ - { - "name": "cleanup_task", - "action": { "type": "delete_record", "target": "task", "payload": { "id": "{{taskId}}" } } - }, - { - "name": "cleanup_project", - "action": { "type": "delete_record", "target": "project", "payload": { "id": "{{projectId}}" } } - } - ] - }, - - { - "id": "S4-overdue-detection", - "name": "Overdue formula and overdue-tasks query work end to end", - "description": "Creates an overdue task and asserts the is_overdue formula evaluates true and that it appears in the overdue query.", - "tags": ["formulas", "reports"], - "setup": [ - { - "name": "seed_project", + "name": "create_tagged_task", "action": { "type": "create_record", - "target": "project", - "payload": { "name": "QA Overdue Project", "key": "QAO", "status": "active" } - }, - "capture": { "projectId": "id" } - }, - { - "name": "seed_overdue_task", - "action": { - "type": "create_record", - "target": "task", + "target": "todo_task", "payload": { - "subject": "QAO — overdue test task", - "project": "{{projectId}}", + "subject": "Fix the login redirect", "status": "todo", - "priority": "high", - "due_date": "2000-01-01" - } - }, - "capture": { "taskId": "id" } - } - ], - "steps": [ - { - "name": "read_overdue_flag", - "action": { - "type": "read_record", - "target": "task", - "payload": { "id": "{{taskId}}" } - }, - "assertions": [ - { "field": "is_overdue", "operator": "equals", "expectedValue": true } - ] - }, - { - "name": "overdue_query_returns_task", - "action": { - "type": "query_records", - "target": "task", - "payload": { - "filter": { - "project": "{{projectId}}", - "status": { "$in": ["todo", "doing"] }, - "due_date": { "$lt": "{today}" } - } + "priority": "urgent", + "labels": ["{{labelId}}"] } }, "assertions": [ - { "field": "data.length", "operator": "gte", "expectedValue": 1 } - ] - }, - { - "name": "completing_clears_overdue", - "description": "Once done, is_overdue must flip to false.", - "action": { - "type": "update_record", - "target": "task", - "payload": { "id": "{{taskId}}", "status": "done" } - }, - "assertions": [ - { "field": "status", "operator": "equals", "expectedValue": "done" }, - { "field": "is_overdue", "operator": "equals", "expectedValue": false } + { "field": "record.labels", "operator": "contains", "expectedValue": "{{labelId}}" } ] } - ], - "teardown": [ - { - "name": "cleanup_task", - "action": { "type": "delete_record", "target": "task", "payload": { "id": "{{taskId}}" } } - }, - { - "name": "cleanup_project", - "action": { "type": "delete_record", "target": "project", "payload": { "id": "{{projectId}}" } } - } ] }, - { - "id": "S5-labels-and-tagging", - "name": "Apply multiple labels to a task via the junction object", - "description": "Tags a task with bug + performance and verifies the junction records exist.", - "tags": ["labels", "junction"], - "setup": [ - { - "name": "seed_project", - "action": { - "type": "create_record", - "target": "project", - "payload": { "name": "QA Label Project", "key": "QLB", "status": "active" } - }, - "capture": { "projectId": "id" } - }, - { - "name": "seed_task", - "action": { - "type": "create_record", - "target": "task", - "payload": { - "subject": "QLB — slow query in dashboard", - "project": "{{projectId}}", - "status": "todo", - "priority": "high" - } - }, - "capture": { "taskId": "id" } - } - ], + "id": "S3-due-date-and-readback", + "name": "Due dates persist and records read back", + "description": "Create a task with a due_date, then read it back by id and confirm the stored fields round-trip through the data API.", + "tags": ["tasks", "read"], "steps": [ { - "name": "lookup_bug_label", - "action": { - "type": "query_records", - "target": "label", - "payload": { "filter": { "name": "bug" }, "limit": 1 } - }, - "capture": { "bugLabelId": "data.0.id" }, - "assertions": [ - { "field": "data.0.name", "operator": "equals", "expectedValue": "bug" } - ] - }, - { - "name": "lookup_perf_label", - "action": { - "type": "query_records", - "target": "label", - "payload": { "filter": { "name": "performance" }, "limit": 1 } - }, - "capture": { "perfLabelId": "data.0.id" } - }, - { - "name": "tag_as_bug", - "action": { - "type": "create_record", - "target": "task_label", - "payload": { "task": "{{taskId}}", "label": "{{bugLabelId}}" } - }, - "capture": { "tagBugId": "id" }, - "assertions": [ - { "field": "id", "operator": "not_null", "expectedValue": null } - ] - }, - { - "name": "tag_as_performance", + "name": "create_dated_task", "action": { "type": "create_record", - "target": "task_label", - "payload": { "task": "{{taskId}}", "label": "{{perfLabelId}}" } - }, - "capture": { "tagPerfId": "id" } - }, - { - "name": "verify_two_tags", - "action": { - "type": "query_records", - "target": "task_label", - "payload": { "filter": { "task": "{{taskId}}" } } - }, - "assertions": [ - { "field": "data.length", "operator": "equals", "expectedValue": 2 } - ] - } - ], - "teardown": [ - { - "name": "cleanup_task", - "description": "Master-detail relation deletes the task_label rows automatically.", - "action": { "type": "delete_record", "target": "task", "payload": { "id": "{{taskId}}" } } - }, - { - "name": "cleanup_project", - "action": { "type": "delete_record", "target": "project", "payload": { "id": "{{projectId}}" } } - } - ] - }, - - { - "id": "S6-validation-guards", - "name": "Validation rules reject invalid records", - "description": "Exercises completed_at_when_done on task and target_after_start on project.", - "tags": ["validations", "negative"], - "setup": [ - { - "name": "seed_project", - "action": { - "type": "create_record", - "target": "project", - "payload": { "name": "QA Validation Project", "key": "QAV", "status": "active" } - }, - "capture": { "projectId": "id" } - } - ], - "steps": [ - { - "name": "reject_completed_at_on_non_done", - "description": "Setting completed_at while status != done must fail.", - "action": { - "type": "create_record", - "target": "task", + "target": "todo_task", "payload": { - "subject": "QAV — invalid completed_at", - "project": "{{projectId}}", + "subject": "Renew the SSL certificate", "status": "todo", - "priority": "normal", - "completed_at": "2025-01-01T00:00:00Z" - } - }, - "assertions": [ - { "field": "error", "operator": "not_null", "expectedValue": null } - ] - }, - { - "name": "reject_target_before_start", - "description": "Project with target_date < start_date must fail target_after_start.", - "action": { - "type": "create_record", - "target": "project", - "payload": { - "name": "QA Invalid Dates", - "key": "QID", - "status": "active", - "start_date": "2030-01-01", - "target_date": "2029-12-01" - } - }, - "assertions": [ - { "field": "error", "operator": "not_null", "expectedValue": null } - ] - } - ], - "teardown": [ - { - "name": "cleanup_project", - "action": { "type": "delete_record", "target": "project", "payload": { "id": "{{projectId}}" } } - } - ] - }, - - { - "id": "S7-state-machine-guard", - "name": "State machine forbids cancelled → done jumps", - "description": "Once a task is cancelled (terminal), it cannot be transitioned back to done. Only an explicit reopen from done is allowed by the state machine.", - "tags": ["state-machine", "negative"], - "setup": [ - { - "name": "seed_project", - "action": { - "type": "create_record", - "target": "project", - "payload": { "name": "QA SM Project", "key": "QSM", "status": "active" } - }, - "capture": { "projectId": "id" } - }, - { - "name": "seed_cancelled_task", - "action": { - "type": "create_record", - "target": "task", - "payload": { - "subject": "QSM — cancelled task", - "project": "{{projectId}}", - "status": "cancelled", - "priority": "low" + "priority": "high", + "due_date": "2020-01-01" } }, - "capture": { "taskId": "id" } - } - ], - "steps": [ - { - "name": "attempt_cancelled_to_done", - "action": { - "type": "update_record", - "target": "task", - "payload": { "id": "{{taskId}}", "status": "done" } - }, + "capture": { "datedId": "id" }, "assertions": [ - { "field": "error", "operator": "not_null", "expectedValue": null } + { "field": "record.due_date", "operator": "contains", "expectedValue": "2020-01-01" } ] - } - ], - "teardown": [ - { - "name": "cleanup_task", - "action": { "type": "delete_record", "target": "task", "payload": { "id": "{{taskId}}" } } }, { - "name": "cleanup_project", - "action": { "type": "delete_record", "target": "project", "payload": { "id": "{{projectId}}" } } - } - ] - }, - - { - "id": "S8-throughput-snapshot", - "name": "Throughput query — tasks completed in the last 30 days", - "description": "Aggregate check against seeded data: at least one done task with completed_at in the recent window.", - "tags": ["analytics", "smoke"], - "steps": [ - { - "name": "query_recent_completions", + "name": "read_back", "action": { - "type": "query_records", - "target": "task", - "payload": { - "filter": { - "status": "done", - "completed_at": { "$ne": null } - }, - "limit": 100 - } + "type": "read_record", + "target": "todo_task", + "payload": { "id": "{{datedId}}" } }, "assertions": [ - { "field": "data.length", "operator": "gte", "expectedValue": 1 } + { "field": "record.id", "operator": "equals", "expectedValue": "{{datedId}}" }, + { + "field": "record.subject", + "operator": "equals", + "expectedValue": "Renew the SSL certificate" + }, + { "field": "record.status", "operator": "equals", "expectedValue": "todo" } ] } ] diff --git a/packages/todo/src/flows/task_assigned.flow.ts b/packages/todo/src/flows/task_assigned.flow.ts index b3cee7a..8fddac6 100644 --- a/packages/todo/src/flows/task_assigned.flow.ts +++ b/packages/todo/src/flows/task_assigned.flow.ts @@ -37,14 +37,13 @@ export const TaskAssignedFlow: Flow = { }, { id: 'notify', - type: 'script', + type: 'notify', label: 'Notify Assignee', config: { - actionType: 'notification', recipients: ['{taskRecord.assignee}'], title: 'New task assigned: {taskRecord.subject}', body: 'You have a new task assigned to you.', - link: '/objects/todo_task/{taskRecord.id}', + actionUrl: '/objects/todo_task/{taskRecord.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/packages/todo/src/flows/task_overdue.flow.ts b/packages/todo/src/flows/task_overdue.flow.ts index 73c6f6f..d6b43d1 100644 --- a/packages/todo/src/flows/task_overdue.flow.ts +++ b/packages/todo/src/flows/task_overdue.flow.ts @@ -38,28 +38,25 @@ export const TaskOverdueFlow: Flow = { }, { id: 'notify', - type: 'script', + type: 'notify', label: 'Notify Assignee', config: { - actionType: 'notification', recipients: ['{taskRecord.assignee}'], title: 'Task overdue: {taskRecord.subject}', body: 'Task "{taskRecord.subject}" is past its due date ({taskRecord.due_date}).', - link: '/objects/todo_task/{taskRecord.id}', + actionUrl: '/objects/todo_task/{taskRecord.id}', }, }, { id: 'email', - type: 'script', + type: 'notify', label: 'Email Assignee', config: { - actionType: 'email', - template: 'todo_task_overdue', + channels: ['email'], recipients: ['{taskRecord.assignee.email}'], - variables: { - subject: '{taskRecord.subject}', - dueDate: '{taskRecord.due_date}', - }, + title: 'Overdue task: {taskRecord.subject}', + body: 'Your task "{taskRecord.subject}" was due {taskRecord.due_date} and is still open.', + actionUrl: '/objects/todo_task/{taskRecord.id}', }, }, { id: 'end', type: 'end', label: 'End' }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48a6e6e..8be3121 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,38 +18,38 @@ importers: packages/compliance: dependencies: '@objectstack/account': - specifier: ^7.3.0 - version: 7.3.0 + specifier: ^7.4.1 + version: 7.4.1 '@objectstack/cli': - specifier: ^7.3.0 - version: 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.3.0(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.4.1 + version: 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.4.1(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@objectstack/driver-memory': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/driver-sql': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/driver-sqlite-wasm': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) '@objectstack/metadata': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/objectql': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/runtime': - specifier: ^7.3.0 - version: 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.4.1 + version: 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@objectstack/service-analytics': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/service-automation': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/spec': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) sql.js: specifier: ^1.14.1 version: 1.14.1 @@ -65,38 +65,38 @@ importers: packages/content: dependencies: '@objectstack/account': - specifier: ^7.3.0 - version: 7.3.0 + specifier: ^7.4.1 + version: 7.4.1 '@objectstack/cli': - specifier: ^7.3.0 - version: 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.3.0(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.4.1 + version: 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.4.1(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@objectstack/driver-memory': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/driver-sql': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/driver-sqlite-wasm': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) '@objectstack/metadata': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/objectql': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/runtime': - specifier: ^7.3.0 - version: 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.4.1 + version: 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@objectstack/service-analytics': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/service-automation': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/spec': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) sql.js: specifier: ^1.14.1 version: 1.14.1 @@ -112,38 +112,38 @@ importers: packages/contracts: dependencies: '@objectstack/account': - specifier: ^7.3.0 - version: 7.3.0 + specifier: ^7.4.1 + version: 7.4.1 '@objectstack/cli': - specifier: ^7.3.0 - version: 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.3.0(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.4.1 + version: 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.4.1(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@objectstack/driver-memory': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/driver-sql': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/driver-sqlite-wasm': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) '@objectstack/metadata': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/objectql': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/runtime': - specifier: ^7.3.0 - version: 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.4.1 + version: 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@objectstack/service-analytics': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/service-automation': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/spec': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) sql.js: specifier: ^1.14.1 version: 1.14.1 @@ -159,38 +159,38 @@ importers: packages/expense: dependencies: '@objectstack/account': - specifier: ^7.3.0 - version: 7.3.0 + specifier: ^7.4.1 + version: 7.4.1 '@objectstack/cli': - specifier: ^7.3.0 - version: 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.3.0(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.4.1 + version: 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.4.1(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@objectstack/driver-memory': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/driver-sql': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/driver-sqlite-wasm': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) '@objectstack/metadata': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/objectql': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/runtime': - specifier: ^7.3.0 - version: 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.4.1 + version: 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@objectstack/service-analytics': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/service-automation': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/spec': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) sql.js: specifier: ^1.14.1 version: 1.14.1 @@ -206,38 +206,38 @@ importers: packages/helpdesk: dependencies: '@objectstack/account': - specifier: ^7.3.0 - version: 7.3.0 + specifier: ^7.4.1 + version: 7.4.1 '@objectstack/cli': - specifier: ^7.3.0 - version: 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.3.0(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.4.1 + version: 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.4.1(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@objectstack/driver-memory': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/driver-sql': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/driver-sqlite-wasm': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) '@objectstack/metadata': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/objectql': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/runtime': - specifier: ^7.3.0 - version: 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.4.1 + version: 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@objectstack/service-analytics': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/service-automation': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/spec': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) sql.js: specifier: ^1.14.1 version: 1.14.1 @@ -253,38 +253,38 @@ importers: packages/hr: dependencies: '@objectstack/account': - specifier: ^7.3.0 - version: 7.3.0 + specifier: ^7.4.1 + version: 7.4.1 '@objectstack/cli': - specifier: ^7.3.0 - version: 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.3.0(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.4.1 + version: 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.4.1(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@objectstack/driver-memory': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/driver-sql': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/driver-sqlite-wasm': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) '@objectstack/metadata': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/objectql': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/runtime': - specifier: ^7.3.0 - version: 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.4.1 + version: 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@objectstack/service-analytics': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/service-automation': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/spec': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) sql.js: specifier: ^1.14.1 version: 1.14.1 @@ -300,38 +300,38 @@ importers: packages/procurement: dependencies: '@objectstack/account': - specifier: ^7.3.0 - version: 7.3.0 + specifier: ^7.4.1 + version: 7.4.1 '@objectstack/cli': - specifier: ^7.3.0 - version: 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.3.0(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.4.1 + version: 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.4.1(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@objectstack/driver-memory': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/driver-sql': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/driver-sqlite-wasm': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) '@objectstack/metadata': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/objectql': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/runtime': - specifier: ^7.3.0 - version: 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.4.1 + version: 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@objectstack/service-analytics': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/service-automation': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/spec': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) sql.js: specifier: ^1.14.1 version: 1.14.1 @@ -347,38 +347,38 @@ importers: packages/project: dependencies: '@objectstack/account': - specifier: ^7.3.0 - version: 7.3.0 + specifier: ^7.4.1 + version: 7.4.1 '@objectstack/cli': - specifier: ^7.3.0 - version: 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.3.0(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.4.1 + version: 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.4.1(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@objectstack/driver-memory': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/driver-sql': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/driver-sqlite-wasm': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) '@objectstack/metadata': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/objectql': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/runtime': - specifier: ^7.3.0 - version: 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.4.1 + version: 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@objectstack/service-analytics': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/service-automation': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/spec': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) sql.js: specifier: ^1.14.1 version: 1.14.1 @@ -394,38 +394,38 @@ importers: packages/todo: dependencies: '@objectstack/account': - specifier: ^7.3.0 - version: 7.3.0 + specifier: ^7.4.1 + version: 7.4.1 '@objectstack/cli': - specifier: ^7.3.0 - version: 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.3.0(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.4.1 + version: 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.4.1(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@objectstack/driver-memory': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/driver-sql': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/driver-sqlite-wasm': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) '@objectstack/metadata': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/objectql': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/runtime': - specifier: ^7.3.0 - version: 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.4.1 + version: 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@objectstack/service-analytics': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/service-automation': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) '@objectstack/spec': - specifier: ^7.3.0 - version: 7.3.0(ai@6.0.191(zod@4.4.3)) + specifier: ^7.4.1 + version: 7.4.1(ai@6.0.191(zod@4.4.3)) sql.js: specifier: ^1.14.1 version: 1.14.1 @@ -753,37 +753,37 @@ packages: resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==} engines: {node: '>= 20.19.0'} - '@objectstack/account@7.3.0': - resolution: {integrity: sha512-+iloxZNeWB+lCyDyZKT+0NdbQ7NZcQSRx/kTihFHkjLfNGZ6zrYkWBaNuNHqGTxEtWG8YdL3u+NmRzQoprLDrQ==} + '@objectstack/account@7.4.1': + resolution: {integrity: sha512-9go4D0/Cx9B12uUDRmJ8i6nWmDMCeIp+gKv1t/d4+3wdwYkEPMb48Kk9mQPo05okKsbf1u2xPIHgwTJgq17+OQ==} - '@objectstack/cli@7.3.0': - resolution: {integrity: sha512-JD4kpkPN4W/3nhZJhy+OpEkz/FPeQWhNWTUlW2Q5PyBE5x6jTLgiI9n0Z1EjkGJDYT4/SHw4l6ILctifJ2bd+g==} + '@objectstack/cli@7.4.1': + resolution: {integrity: sha512-r0CSlP5cJPO6JmQbHlY0fzFPugW43Z+ZP3I+3sROqtAgi4ZYW915K5022ODcKewetPgmQFPOY14oHHpPmeFW4w==} engines: {node: '>=18.0.0'} hasBin: true peerDependencies: - '@objectstack/core': ^7.3.0 + '@objectstack/core': ^7.4.1 - '@objectstack/client@7.3.0': - resolution: {integrity: sha512-8yR8eLErWe9m75xgbNF8q1kdwEdhxckOAaIQ3kAheUX82eFMJZ3fRKK7d5vppymfmuSPIdFsi9PKnI47pHSeqQ==} + '@objectstack/client@7.4.1': + resolution: {integrity: sha512-2r6c5N81/udlGwCb9fCAi+pJnonLZ6bqD5nUBA96C6kwmnRShI3L8m3E/EpAwqRoPq0vN1iMlHXblII8Zq1Itg==} engines: {node: '>=18.0.0'} - '@objectstack/console@7.3.0': - resolution: {integrity: sha512-SdwlbWLcDXvklePmezjeK4cGR2BiNl0cwbiFPDclQn0B/zGsVyfXWYZRJg/y0sRgy01GMXrc9iZasplk71Qa5A==} + '@objectstack/console@7.4.1': + resolution: {integrity: sha512-MDOgfm2JtJrlh1UI/yxR3E9BFX5SOM3OJrkw+BcTTKWNnBQGoX4thqwdjwwTYBxQmVr1D/AhhOMIm1uQETQx8g==} - '@objectstack/core@7.3.0': - resolution: {integrity: sha512-DFyI3DF/rQnN2Ld2ZCatr+OMh6uMVhrHeUnewcSWgVkxph6aXJS7JdUdTkxj0j0jBTGhNFcZxAwASp8QwVXdKQ==} + '@objectstack/core@7.4.1': + resolution: {integrity: sha512-GLUp0GLW8OvC0RXEj5jhdDiLgvslQ9WFw77NTzr5hr5ZqAZNTeM7k6JPSSsB1g4whkqCSAK2sIgWMoc3lXNZpA==} engines: {node: '>=18.0.0'} - '@objectstack/driver-memory@7.3.0': - resolution: {integrity: sha512-p5smsPH8Q0HhA9nae09manaGxdoY8uv5Ji1R/qinjhNdag3T093bDdlnLxGFH9ChIo/OTaAsfXipgk6B+Itc1w==} + '@objectstack/driver-memory@7.4.1': + resolution: {integrity: sha512-ImdZK+pOYQhPKk4zdAC5YQUSv4EkpwZ6f88sv91ZIvkbKl4XvGOVEqpWQNTY3+Y1rgd0bZz83G60qjOhsZxvNg==} engines: {node: '>=18.0.0'} - '@objectstack/driver-mongodb@7.3.0': - resolution: {integrity: sha512-ugwVkse9rxphAr2crA8vCzO5HwLyQCmGekO7rOeFdbBILHrN6jAwcO5jte1Nbwbzr6tNFrRJMOTYgsT3ZeO3Kw==} + '@objectstack/driver-mongodb@7.4.1': + resolution: {integrity: sha512-RespT9stSFaV045p3+0/p6bPgt8L2KwfIErQUjcgqrv2JBTKV4LahB3IIq8Q0yY9t4JcG8W7jreua3v3FHAdmA==} engines: {node: '>=18.0.0'} - '@objectstack/driver-sql@7.3.0': - resolution: {integrity: sha512-5r8fo3yluuBfc8vEiw3X1/+yeG79XQUCaHTWdCp/KVCH40LGRpjWWyCc+5UsVWwd3ZsTFIG9xTGM8REwCuBLHQ==} + '@objectstack/driver-sql@7.4.1': + resolution: {integrity: sha512-icy6kC3lAdv/rz7BV0OM8LWBFx/JRMl7zdQdi8JpzHnwZOIr8q3Rmj9H0u4+bXdIUMv3T+vz3U8qpffk9TFZdA==} engines: {node: '>=18.0.0'} peerDependencies: mysql2: ^3.0.0 @@ -800,15 +800,15 @@ packages: tedious: optional: true - '@objectstack/driver-sqlite-wasm@7.3.0': - resolution: {integrity: sha512-+9pjpNRLBTuclev9SKL8dFlSFNVPhU8Qnasax8sP1sH0nvttaMD9zZs44BjjE3rmkhGM829OSJycO4N+324umA==} + '@objectstack/driver-sqlite-wasm@7.4.1': + resolution: {integrity: sha512-wjhFciCrukL7ukzMy66c+tthJnl6DJuxcnzqdzpPSA6awrUPckWueuCJY6c5QEb4YO6Ro5vxbj7yv/qoC5MZrA==} engines: {node: '>=18.0.0'} - '@objectstack/formula@7.3.0': - resolution: {integrity: sha512-Gs8BTKi/i8RnpMAFBqgdYPC+I2w7H2NP7fE8FlzQCqDB56H5hsqZGwJaZ0jFp9lGtorBpV/Lcg/kyXcM021aNg==} + '@objectstack/formula@7.4.1': + resolution: {integrity: sha512-1xuKWnwsIaOdGwioSCL9i86nyA27G4RyZ3eln3o4xmxXnv2b18bmvBvQWCF264dXVYVXIPf66bW9lqFFXhD/vg==} - '@objectstack/metadata-core@7.3.0': - resolution: {integrity: sha512-jbH5ZkCmKCDr+XKPrh3Ei0+q+jbt5w0r7dBmdgJFqXp7xc01qcPkCmB33PBQGXSrPfjWNP8KXDl5euYtNQ/s4A==} + '@objectstack/metadata-core@7.4.1': + resolution: {integrity: sha512-WIW3yUHRVgOyJPvRmAcwgDdEkIDbOE4oJClpKFH2v2dwdfFLaIFRV7ZZKoXB95H3zO7LY75rmbYhUtCuqahlkQ==} engines: {node: '>=18.0.0'} peerDependencies: vitest: ^4.0.0 @@ -816,82 +816,90 @@ packages: vitest: optional: true - '@objectstack/metadata-fs@7.3.0': - resolution: {integrity: sha512-FCkhImn7krA/ujuAza30cQICHCuE9gOSFAQNGig54hjRgL4eWg9Ug+rmBk+PSCVYuHuxqEh1o7j9cBQbhJpozA==} + '@objectstack/metadata-fs@7.4.1': + resolution: {integrity: sha512-JN/xYNlGLOOZU6dIFGqP8YBCLSiyltEnLySs4cHH7HfD7/VxsNcwWfjkOe3W5rvJnbDaxtmcN41sfRFVvDaxmw==} engines: {node: '>=18.0.0'} - '@objectstack/metadata@7.3.0': - resolution: {integrity: sha512-JtbS3tnHUP+hOkekCYMuUJ1eIhPqPuRK2kf1dkcs27SaFw3DbwzXZXYcmVx54IwwCqvtUPTP5ztc1vQsKNRqHQ==} + '@objectstack/metadata@7.4.1': + resolution: {integrity: sha512-TD3aDiOhLy4eS6EAkL/x2/cbMpWGDOdkn4v2GC7f2fW//4mUEC6kYnhdI+d4yTHMDRtWa8INRBH7AR2cBb9aTg==} engines: {node: '>=18.0.0'} - '@objectstack/objectql@7.3.0': - resolution: {integrity: sha512-Ny16vWXoEJuA6doikiJXFlm0qQsLdUCK4Ezm4vvVUNMfzU9TC1jIXDmF/cZEPNyVc7r5gPdSX0viJWmvdHy/Gg==} + '@objectstack/objectql@7.4.1': + resolution: {integrity: sha512-VDM7+DPB6hyiFp2SqRvpeaiXac712FvpPxEB4Uq3iVdfIS8bbG3v/1Aqy4Tfm13kCVlHo6vcMCZgkxR1WK0+OA==} engines: {node: '>=18.0.0'} - '@objectstack/observability@7.3.0': - resolution: {integrity: sha512-xmM1cp37DC3NDxrIAuEe1z0v26Sh6vbzXtqaWJKZrXGRAMcmq2497AawwNP0DPjxhDIMgGvkI8WNvSHFQ4OkWw==} + '@objectstack/observability@7.4.1': + resolution: {integrity: sha512-3iZ0IQ6Y/4Vq8xQjpVy3d18UdM4u1d1UpPJ602rhPOrMsaIQWP1kHEPT/Y143BkBl2deQRcpX7547X8sEC3fYg==} engines: {node: '>=18.0.0'} - '@objectstack/platform-objects@7.3.0': - resolution: {integrity: sha512-j+aO47bI0fj1O/M/9O16N/wJpT8Qdstzufmto6jCj2eHowjq57hs/3x60cSDnMfadZgugwGqhrjEKElbN/jjjw==} + '@objectstack/platform-objects@7.4.1': + resolution: {integrity: sha512-WBdw6tUqZPm7REo3P3FiWtN3+b9vkuayfa84+iRm/kFt+kxMy789J4XHG9in1+BPigcgizuUOmO+AYvy/eOekg==} engines: {node: '>=18.0.0'} - '@objectstack/plugin-approvals@7.3.0': - resolution: {integrity: sha512-h/zG96yufozh0TUdhncZPHU6e4iFS/YNMWRrPBIy/Kmo9ikUMKTqjmgAd1FTWADJQHS1cDJCQVg5eqaOUU541w==} + '@objectstack/plugin-approvals@7.4.1': + resolution: {integrity: sha512-DhO7kE+4/7DZ8HFEbjIwzlE36IkuN+Wt4G+TqY1HmmNWR4um4i6c3qUR0jXRdLFtROm1sRKZjIsHQdutAA8LpQ==} - '@objectstack/plugin-audit@7.3.0': - resolution: {integrity: sha512-SqBPGrRgsVAWCBV9g2K6P4aiOyrmBq1PojMCxgAmIMXvpz9QzCo0nLGcKbxVuzhrdtlavkj5PdA3DppBC0U4aQ==} + '@objectstack/plugin-audit@7.4.1': + resolution: {integrity: sha512-yxNVUbMJIPee+nqaB38Dg/g33nvaKrA+/r15QiW+jyDhuNCyrW9QtPcp1shJXAh8I+wBMVQPtDhGacMb/pLvvA==} engines: {node: '>=18.0.0'} - '@objectstack/plugin-auth@7.3.0': - resolution: {integrity: sha512-e0uzkJRt6l/fLt+vpPiPID5AxTz4TZVvODC5o87eh0xBc90OVaOdGbVFgCjQGV65SkibkBPbVPSjW90peEtXLw==} + '@objectstack/plugin-auth@7.4.1': + resolution: {integrity: sha512-rQMf6ZUw5qNqhuj8NSx6zuAtPolM2TuwF/VO0YTJH1n1a1Us551J7ev53Htp6uNA+UUJB0w2Yop0yWXXCTPpyw==} engines: {node: '>=18.0.0'} - '@objectstack/plugin-email@7.3.0': - resolution: {integrity: sha512-bBHn9k26c0R9raGEdv26d7xzze48ZHKYaoTs8nI2f0yX8NNLL1bnT+tblLpcX4+b0WFSN6/F62k+w4R8htWkEg==} + '@objectstack/plugin-email@7.4.1': + resolution: {integrity: sha512-snADUNkSxZdl/eBoZcSDam2aAj30feOOgB+NgV3aYbE1hkXNN1uWo8Ckl6IvKjDFmGvhe9zQcxmIkBQSButNlg==} - '@objectstack/plugin-hono-server@7.3.0': - resolution: {integrity: sha512-K/xcg22Aj6FLdQ8QJQF7CMQ4WYecWM2xO0a+UAMuLiCPUqAsU4zHfTdM9QzyqPtfRmk1s1ftBo6/R57rGWhVsw==} + '@objectstack/plugin-hono-server@7.4.1': + resolution: {integrity: sha512-XCUtvxiLT2VLq8PgW0kLTpsNZLKHcI42IGdVrkEyBXrKkgrd6Kiq2xjo5KXCdfbhVl+IE1N0KiE7CrMeUcTGSg==} engines: {node: '>=18.0.0'} - '@objectstack/plugin-mcp-server@7.3.0': - resolution: {integrity: sha512-aKu8E/nBI4mFNoOV0FYlGKytLLiHM9PE9PoVKAI3uKm4y6+qGaXGkfK7koT4IEB7Eq7nS9qlGElyaL3FHVdwuA==} + '@objectstack/plugin-mcp-server@7.4.1': + resolution: {integrity: sha512-t3oRmQTiTrTQ9QO4KarVGUDifTenm+NeoZsClBLreiRn6PRi+zkW5yrhnW6VU8TydMVkVyUgcg/S2oX8uXerzQ==} engines: {node: '>=18.0.0'} - '@objectstack/plugin-org-scoping@7.3.0': - resolution: {integrity: sha512-9Yfq7Hpzn2RkLaMrmNGuhOmKrkDHpgX7pZSNF2bZyXI76y/1vHxzQKfAkLkkuL7gW6DUXCGQ76KQOsvC4Y423A==} + '@objectstack/plugin-org-scoping@7.4.1': + resolution: {integrity: sha512-G10oetqZUfu6Ozi0FB4pPQgfytgjUIw4Z8Kyl5RhIt+5XGBNR/JNbZqezzBE9eKCIJrKMF0uEH3S38sJaKpucg==} engines: {node: '>=18.0.0'} - '@objectstack/plugin-reports@7.3.0': - resolution: {integrity: sha512-XEPhIyVP/iwlRdGuPggOfqc3+5vwQQeJycNh20KVqPoVA+i/201PrTIODWynaYwb7aF0AaoEH5kiOZhaMHLvqQ==} + '@objectstack/plugin-reports@7.4.1': + resolution: {integrity: sha512-ZV+SpnOdyjFWYsVe4Dmnacc0vGHZNl+UhOQLK967xojzrX4hk8W6mM1wEGbzuHptahBq4tcbXg4SnGQKJeEfVA==} - '@objectstack/plugin-security@7.3.0': - resolution: {integrity: sha512-XZyS4c9EopR2xNaW1vB2RAWpzmNpjNd/LEOvClRxU9QbOh0zk9j+qjYAyDceNSnlWCfdWYophg0Xv6W/wVABUQ==} + '@objectstack/plugin-security@7.4.1': + resolution: {integrity: sha512-6svrUYMfHlY01S8cibfHjKeokJL4fKh/0C9hjMse8vSBq1dowr3bwaCoTZXEf7kodJXTtXAeba9Cnn11s5I65g==} engines: {node: '>=18.0.0'} - '@objectstack/plugin-sharing@7.3.0': - resolution: {integrity: sha512-fUncnb4ghAYFV95Gj3q22A+zIpuAnbyA5KFnw4hoDIlDcd9SeSJ+h8E4N8uOPc7WjfPeYP/HglBChklKg8UldQ==} + '@objectstack/plugin-sharing@7.4.1': + resolution: {integrity: sha512-wMrYQa4z5mk1DLnTzGdygu9dmWJZ5LxoYgtAeIwJR3y9Oelo7HssaEOS2bWF1s2wvuFeXV93ovdubxsHJ3cfZw==} - '@objectstack/plugin-webhooks@7.3.0': - resolution: {integrity: sha512-/WdLQ3As2ysxHt7RIfjdQIoFPLXPc4X+BsQvo97T5Cxd2X4v7zLJT6svYMC6CsDmyFQJWFFmhILo+fzn0NRcgQ==} + '@objectstack/plugin-trigger-record-change@7.4.1': + resolution: {integrity: sha512-BfsZNSc3Wdm3tVzDXPdnveuj5DlnDh9ii+THqCwBtN68HuOBhnkxK9aRUopZhLAf9DaVe6wOnUMLN+TzXsq3yQ==} + engines: {node: '>=18.0.0'} + + '@objectstack/plugin-trigger-schedule@7.4.1': + resolution: {integrity: sha512-ztrD2RfXOeeUY+Opo4acPzc/gjYaioRos5y0ePSNneE79P4fp5GAIGSjJP/nt87sq8qLCqLjAiMxsxkVs3z8xA==} + engines: {node: '>=18.0.0'} - '@objectstack/rest@7.3.0': - resolution: {integrity: sha512-BVa8qYRjjTsgXhbUxuSmMCht5n9UFnIAue1Y5lHstZPIoc06izHmC3lpYiNY+69OxhQDioWzUIZybORgeFjTgA==} + '@objectstack/plugin-webhooks@7.4.1': + resolution: {integrity: sha512-+GqvLA7ww05N6zTAfMx+QBPj4vJMztlDG5lKg1WeiGjilJeh0AOBXudYtAtCKOL+zE1WGw4+WPeyndXIt471Hw==} + + '@objectstack/rest@7.4.1': + resolution: {integrity: sha512-58T4uHeDbN9KvvG90z9ufWqPjJuds5XBeWUu9/zWS10HOrTzBxFnjCwkgdtf1YGn+rXRqPysqgicrjRmiR1kWQ==} engines: {node: '>=18.0.0'} - '@objectstack/runtime@7.3.0': - resolution: {integrity: sha512-w2dIr5cNxNxKUI0m03tbvzmTxLCHOl0rQWeIqRqeF9rYSX0LXIulO/tSmOEUCAFxlcnm+xQFMT6GWmz6xZpmUg==} + '@objectstack/runtime@7.4.1': + resolution: {integrity: sha512-wrajvZ17QaSoNYuht+o89zI1AcuvL8haLqXp4tgf6LkOCc1toCQHMbJq5Ywgt73Y4f7CUIlUTWzfMBN+Q7C8+g==} engines: {node: '>=18.0.0'} - '@objectstack/service-ai@7.3.0': - resolution: {integrity: sha512-PWn6Hcchzc/JUfDLC5nK9RR3bw9gYUI901sb9/tOZ4utSKcBc1N2RHYQvIHGJBJYNA5H3crcN4C5ZugzKYMNww==} + '@objectstack/service-ai@7.4.1': + resolution: {integrity: sha512-/QhTXgLYbCYsLjq8ufxTd7ldKALa4tjUksYJPeP2V4VXwGO3wg63s7iI6Xcs5q+Pf7xUfnCP1fKePb5M90QhWg==} engines: {node: '>=18.0.0'} peerDependencies: '@ai-sdk/anthropic': ^3.0.0 '@ai-sdk/gateway': ^3.0.0 '@ai-sdk/google': ^3.0.0 '@ai-sdk/openai': ^3.0.0 - '@objectstack/embedder-openai': ^7.3.0 + '@objectstack/embedder-openai': ^7.4.1 peerDependenciesMeta: '@ai-sdk/anthropic': optional: true @@ -904,51 +912,58 @@ packages: '@objectstack/embedder-openai': optional: true - '@objectstack/service-analytics@7.3.0': - resolution: {integrity: sha512-WjnHqCYApG8b9oI8IbhLeUANBz/jQ6s43y8Tcr6DnwrhBWGYIlkVDweHBXfUELfxNhzIJsYyukCwxZbgjRXhJg==} + '@objectstack/service-analytics@7.4.1': + resolution: {integrity: sha512-8tOK4ocCZxADr3HheTfHuBN+fJRSS89D8uhJMuktZkbNhRba8GUK0QPqoUAahCc+KJb9pGLHfyAInXeJT5rNSA==} engines: {node: '>=18.0.0'} - '@objectstack/service-automation@7.3.0': - resolution: {integrity: sha512-t8UByh/auBQudD8eXzj3joQC+i+Ki5Fy+bzX7UkoSsGOXIuUC1l5cAHJSGHXIfNrQMO/oB9Kv19jZ0HyV/8QhQ==} + '@objectstack/service-automation@7.4.1': + resolution: {integrity: sha512-ZliFFXvQBc3g/uQQgulk2XgU0yL51cL5Kb4+Byl+RA9i9ravplmYweFPkyBHKyudodSDwJxm6b/XoiuGtrHU9A==} engines: {node: '>=18.0.0'} - '@objectstack/service-cache@7.3.0': - resolution: {integrity: sha512-sKcN+w30gMA13SJM5umVGkEUhL6xoy+BkQK2gkiTlcZtMH1OiYv9i++ZqrNcGmRHWhwqQskbDpvKWyjGwQmCPQ==} + '@objectstack/service-cache@7.4.1': + resolution: {integrity: sha512-obuLlY9kKsMBtFeOCvViJ6z3faoLLQTyqchUqW07l0fa8Z4iVuTE3rcDOcKASx+mslKsGimLBb3vb3l3/OAvRA==} engines: {node: '>=18.0.0'} - '@objectstack/service-cluster@7.3.0': - resolution: {integrity: sha512-cgQeIbvlNX/9V2TFX02nbJ1JFY6pimnLisffPt88IBj//P1s0WVEtqJmOc1Ev7KpDS+JsgjrcYyotUe1cDA6Sg==} + '@objectstack/service-cluster@7.4.1': + resolution: {integrity: sha512-w6/lS9HIoDg1wfsJHni28lt3RkMqpWdaGZ1CcYfUtHtAf/5cqzuhpDsx+FJsom/zweSlAn83BSg832P22EiaPQ==} + + '@objectstack/service-external-datasource@7.4.1': + resolution: {integrity: sha512-bKzIC94Ra2InJzuLwdPjj/xlgDnzLdjWohBbN3JkNf8AxFwNOs7EOTRaglt76/ZjPVvs3ynAyFUJ2CgEZQ8O3w==} + + '@objectstack/service-feed@7.4.1': + resolution: {integrity: sha512-b1/ekT9pVjtqGJAGgangt+vO+YvhIDiIu9T2j3PkjqPUNOZfMyUjWhIN+iQB/a3FxWVlIpWvkZW7memakpSZaw==} + engines: {node: '>=18.0.0'} - '@objectstack/service-feed@7.3.0': - resolution: {integrity: sha512-j7062ejPcotfVo2OvNAKcQozKFvbyus2W5Z85yNr99Mu+yyu0PiiJoUa3RqBpgQf3/7Fns57aLi0uH3FtLjSoQ==} + '@objectstack/service-i18n@7.4.1': + resolution: {integrity: sha512-3miTbUwt2w30YxneTBV59Lb3FWPFdygB1kkrfK84lFMEcTR9n4PgaoFX7KangqhDTqS4z7CR1I9EoCw+yH/p/g==} engines: {node: '>=18.0.0'} - '@objectstack/service-i18n@7.3.0': - resolution: {integrity: sha512-bTHfDSpTx29MjNKbvCPuo5Jp9cDVE0msa55w7dtfADqZMuZoXBTrfJJmzGDU8bqR5HV2wpSkwMzPTGuUeTMInQ==} + '@objectstack/service-job@7.4.1': + resolution: {integrity: sha512-qEBLZQfolY/hMOcRKVXZI61vYglOeAomOCz6w2eI1MW1b0bSoyRplBGHUMWZDyAT1KTfY7l+SOrGmkQHmoy26A==} engines: {node: '>=18.0.0'} - '@objectstack/service-job@7.3.0': - resolution: {integrity: sha512-LnsU3ZMT6AcuvxaOxAXlWjqnQfGix4ItTiRaoZoXtLmkI8pbdtHQdkeZP6vz+KJlBVEKGPcKtlVTWzoqQdAIXQ==} + '@objectstack/service-messaging@7.4.1': + resolution: {integrity: sha512-Z/GP+l+GOPUF/Hq/6pOQ7Hl4/HYkdZPNDhtog7KtxUjIK/FiJXVlHHUhd9G+bUYGodnNonStaXlR1RQttu/wtA==} engines: {node: '>=18.0.0'} - '@objectstack/service-package@7.3.0': - resolution: {integrity: sha512-ngORUP+i3ekiycRgsktVfc5ZB18muLN3lJSrDL12zHHTrBZY2yMvmDl8WcuA5LMOtftEMQ8ECFI5GagYdjYiKQ==} + '@objectstack/service-package@7.4.1': + resolution: {integrity: sha512-2XTRKN8ZTfa9MFdIB0gz50/JvDN6iUurswzyvqB4dHP5mL9RW6S2mWf42DDF+jEm1FYMqPDOu7iip7dqJZDBGw==} engines: {node: '>=18.0.0'} - '@objectstack/service-queue@7.3.0': - resolution: {integrity: sha512-jioGSVTjrbR2enyNiHG5iZAQjLG58GYByqSTr8jJCjLxa8A8Ru5AdEFRTYWI/mnpRu0KXypzkCHDtW8/k6qp4w==} + '@objectstack/service-queue@7.4.1': + resolution: {integrity: sha512-xnJSM4f0PCh9nLsAPdi45hRkSlF+deflfUBCsCUBJS0kMl8rFlZKoumFHJ0/XpTw+mfhmtpN+qQxWXWhyyg37w==} engines: {node: '>=18.0.0'} - '@objectstack/service-realtime@7.3.0': - resolution: {integrity: sha512-n9STUME3ToBaf4Kiv5Xh0cJw4tmcsSkw9YL3cIDWuU1FKP1kEHVEOL0TmZnk61tg58/ll0S4nusaFWKDdUpGwQ==} + '@objectstack/service-realtime@7.4.1': + resolution: {integrity: sha512-UzQxdNlET1kMMefsryB6oIP69Y5bNOCBoczGKEzx5UDlS+B5vusLVo2wUlhvqG0Kw6Uju2kcG7XyrADJXi9TZw==} engines: {node: '>=18.0.0'} - '@objectstack/service-settings@7.3.0': - resolution: {integrity: sha512-MK7QWoDwm05vVkGJD7MJXwboXTsouuxD5uIfTtQiEHZKEO/vZgn0JKHpaKzq6CeXue65gU+FoKd4a2Z2jbGlfQ==} + '@objectstack/service-settings@7.4.1': + resolution: {integrity: sha512-xMPJcW5JE0bHRNg5A1jkfiJ0seiPPZmDV3H3KvgS1kR2+IZTE5lasKG1n8LyIcOwx2FVxKx5cKOKaDq2VPj3AA==} engines: {node: '>=18.0.0'} - '@objectstack/service-storage@7.3.0': - resolution: {integrity: sha512-3S1N8nTcYG3HpbEh3cid2N0cT2Au7YDKtxQeRZzHU4E0c/xfLXJ/TyWFxYtX3mCPy4Ji/nYoA8MR2bNgDox+Iw==} + '@objectstack/service-storage@7.4.1': + resolution: {integrity: sha512-GYzJ2RCDylIiLBeMVXPrJG32kBxFdQo0byQS6q85SV/ZJ8zg9hOasyW9/iWssxM/dur62juzp65SYxIcYikFiA==} engines: {node: '>=18.0.0'} peerDependencies: '@aws-sdk/client-s3': ^3.0.0 @@ -959,8 +974,8 @@ packages: '@aws-sdk/s3-request-presigner': optional: true - '@objectstack/spec@7.3.0': - resolution: {integrity: sha512-yoa721aRJ+9w59PpKeDW4odNGk1Quy2OhOz0RwqTHTMxX3hC+iyufcLxu5C7k0VPbE/pM51yZ+fXsvxNBi246w==} + '@objectstack/spec@7.4.1': + resolution: {integrity: sha512-sHzH2csNuliOvW3IevBJgMAjoEldrN8dTSfQJjsxE5jOlRflW4Yx2Y4bi8/Ek1vfutk+TQcfzVgrrMd/2tiFFg==} engines: {node: '>=18.0.0'} peerDependencies: ai: ^6.0.0 @@ -968,8 +983,8 @@ packages: ai: optional: true - '@objectstack/types@7.3.0': - resolution: {integrity: sha512-aAJPEEfiO7reB+T4+MsuvGrNgxIxXXYQmX7OQZKixkGed7/9BoehVgEOVNT6QKn9sg9MC/zIOLzRxaq5B6rS6Q==} + '@objectstack/types@7.4.1': + resolution: {integrity: sha512-GF2odO9Ra09YiWUJel8sVKr3bi1oTpxsDWhMV382H+nd09hIUx4QEySVOncMDgBMgNepcYPPofUeKGA48bCKNQ==} engines: {node: '>=18.0.0'} '@oclif/core@4.11.4': @@ -2230,48 +2245,52 @@ snapshots: '@noble/hashes@2.2.0': {} - '@objectstack/account@7.3.0': {} + '@objectstack/account@7.4.1': {} - '@objectstack/cli@7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.3.0(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@objectstack/cli@7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@objectstack/core@7.4.1(ai@6.0.191(zod@4.4.3)))(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@ai-sdk/gateway': 3.0.120(zod@4.4.3) - '@objectstack/account': 7.3.0 - '@objectstack/client': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/console': 7.3.0 - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/driver-memory': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/driver-mongodb': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/driver-sql': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/driver-sqlite-wasm': 7.3.0(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) - '@objectstack/objectql': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/observability': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/platform-objects': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/plugin-approvals': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/plugin-audit': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/plugin-auth': 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@objectstack/plugin-email': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/plugin-hono-server': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/plugin-mcp-server': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/plugin-org-scoping': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/plugin-reports': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/plugin-security': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/plugin-sharing': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/plugin-webhooks': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/rest': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/runtime': 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@objectstack/service-ai': 7.3.0(@ai-sdk/gateway@3.0.120(zod@4.4.3)) - '@objectstack/service-analytics': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/service-automation': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/service-cache': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/service-feed': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/service-job': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/service-package': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/service-queue': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/service-realtime': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/service-settings': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/service-storage': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/types': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/account': 7.4.1 + '@objectstack/client': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/console': 7.4.1 + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/driver-memory': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/driver-mongodb': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/driver-sql': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/driver-sqlite-wasm': 7.4.1(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) + '@objectstack/objectql': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/observability': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/platform-objects': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/plugin-approvals': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/plugin-audit': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/plugin-auth': 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@objectstack/plugin-email': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/plugin-hono-server': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/plugin-mcp-server': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/plugin-org-scoping': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/plugin-reports': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/plugin-security': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/plugin-sharing': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/plugin-trigger-record-change': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/plugin-trigger-schedule': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/plugin-webhooks': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/rest': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/runtime': 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@objectstack/service-ai': 7.4.1(@ai-sdk/gateway@3.0.120(zod@4.4.3)) + '@objectstack/service-analytics': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/service-automation': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/service-cache': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/service-external-datasource': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/service-feed': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/service-job': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/service-messaging': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/service-package': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/service-queue': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/service-realtime': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/service-settings': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/service-storage': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/types': 7.4.1(ai@6.0.191(zod@4.4.3)) '@oclif/core': 4.11.4 bundle-require: 5.1.0(esbuild@0.28.0) chalk: 5.6.2 @@ -2332,34 +2351,34 @@ snapshots: - vitest - vue - '@objectstack/client@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/client@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/console@7.3.0': {} + '@objectstack/console@7.4.1': {} - '@objectstack/core@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/core@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) zod: 4.4.3 transitivePeerDependencies: - ai - '@objectstack/driver-memory@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/driver-memory@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) mingo: 7.2.1 transitivePeerDependencies: - ai - '@objectstack/driver-mongodb@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/driver-mongodb@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) mongodb: 7.2.0 nanoid: 5.1.11 transitivePeerDependencies: @@ -2372,10 +2391,10 @@ snapshots: - snappy - socks - '@objectstack/driver-sql@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/driver-sql@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) knex: 3.2.10(better-sqlite3@12.10.0) nanoid: 5.1.11 optionalDependencies: @@ -2387,11 +2406,11 @@ snapshots: - pg-query-stream - supports-color - '@objectstack/driver-sqlite-wasm@7.3.0(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0)': + '@objectstack/driver-sqlite-wasm@7.4.1(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0)': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/driver-sql': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/driver-sql': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) knex: 3.2.10(better-sqlite3@12.10.0) nanoid: 5.1.11 sql.js: 1.14.1 @@ -2407,32 +2426,32 @@ snapshots: - supports-color - tedious - '@objectstack/formula@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/formula@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: '@marcbachmann/cel-js': 7.6.1 - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/metadata-core@7.3.0': + '@objectstack/metadata-core@7.4.1': dependencies: zod: 4.4.3 - '@objectstack/metadata-fs@7.3.0': + '@objectstack/metadata-fs@7.4.1': dependencies: - '@objectstack/metadata-core': 7.3.0 + '@objectstack/metadata-core': 7.4.1 chokidar: 5.0.0 transitivePeerDependencies: - vitest - '@objectstack/metadata@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/metadata@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/metadata-core': 7.3.0 - '@objectstack/metadata-fs': 7.3.0 - '@objectstack/platform-objects': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/types': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/metadata-core': 7.4.1 + '@objectstack/metadata-fs': 7.4.1 + '@objectstack/platform-objects': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/types': 7.4.1(ai@6.0.191(zod@4.4.3)) chokidar: 5.0.0 glob: 13.0.6 js-yaml: 4.1.1 @@ -2441,58 +2460,58 @@ snapshots: - ai - vitest - '@objectstack/objectql@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/objectql@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/formula': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/metadata-core': 7.3.0 - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/types': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/formula': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/metadata-core': 7.4.1 + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/types': 7.4.1(ai@6.0.191(zod@4.4.3)) zod: 4.4.3 transitivePeerDependencies: - ai - vitest - '@objectstack/observability@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/observability@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/platform-objects@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/platform-objects@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/plugin-approvals@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/plugin-approvals@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/formula': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/metadata-core': 7.3.0 - '@objectstack/platform-objects': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/formula': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/metadata-core': 7.4.1 + '@objectstack/platform-objects': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - vitest - '@objectstack/plugin-audit@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/plugin-audit@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/platform-objects': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/platform-objects': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/plugin-auth@7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@objectstack/plugin-auth@7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/oauth-provider': 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-auth@1.6.11(@opentelemetry/api@1.9.1)(better-sqlite3@12.10.0)(mongodb@7.2.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(better-call@1.3.5(zod@4.4.3)) '@noble/hashes': 2.2.0 - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/platform-objects': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/types': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/platform-objects': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/types': 7.4.1(ai@6.0.191(zod@4.4.3)) better-auth: 1.6.11(@opentelemetry/api@1.9.1)(better-sqlite3@12.10.0)(mongodb@7.2.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) transitivePeerDependencies: - '@better-auth/utils' @@ -2524,111 +2543,125 @@ snapshots: - vitest - vue - '@objectstack/plugin-email@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/plugin-email@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/platform-objects': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/platform-objects': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/plugin-hono-server@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/plugin-hono-server@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: '@hono/node-server': 2.0.4(hono@4.12.23) - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/types': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/types': 7.4.1(ai@6.0.191(zod@4.4.3)) hono: 4.12.23 transitivePeerDependencies: - ai - '@objectstack/plugin-mcp-server@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/plugin-mcp-server@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/types': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/types': 7.4.1(ai@6.0.191(zod@4.4.3)) zod: 4.4.3 transitivePeerDependencies: - '@cfworker/json-schema' - ai - supports-color - '@objectstack/plugin-org-scoping@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/plugin-org-scoping@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/platform-objects': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/platform-objects': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/plugin-reports@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/plugin-reports@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/platform-objects': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/platform-objects': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/plugin-security@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/plugin-security@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/platform-objects': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/platform-objects': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/plugin-sharing@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/plugin-sharing@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/objectql': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/platform-objects': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/objectql': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/platform-objects': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - vitest - '@objectstack/plugin-webhooks@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/plugin-trigger-record-change@7.4.1(ai@6.0.191(zod@4.4.3))': + dependencies: + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) + transitivePeerDependencies: + - ai + + '@objectstack/plugin-trigger-schedule@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/platform-objects': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/service-cluster': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/types': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/rest@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/plugin-webhooks@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/service-package': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/platform-objects': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/service-cluster': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/types': 7.4.1(ai@6.0.191(zod@4.4.3)) + transitivePeerDependencies: + - ai + + '@objectstack/rest@7.4.1(ai@6.0.191(zod@4.4.3))': + dependencies: + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/service-package': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) zod: 4.4.3 transitivePeerDependencies: - ai - '@objectstack/runtime@7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/driver-memory': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/driver-sql': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/driver-sqlite-wasm': 7.3.0(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) - '@objectstack/formula': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/metadata': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/objectql': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/observability': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/plugin-auth': 7.3.0(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@objectstack/plugin-org-scoping': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/plugin-security': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/rest': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/service-cluster': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/service-i18n': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/types': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/runtime@7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/driver-memory': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/driver-sql': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/driver-sqlite-wasm': 7.4.1(ai@6.0.191(zod@4.4.3))(better-sqlite3@12.10.0) + '@objectstack/formula': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/metadata': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/objectql': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/observability': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/plugin-auth': 7.4.1(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.191(zod@4.4.3))(better-call@1.3.5(zod@4.4.3))(better-sqlite3@12.10.0)(jose@6.2.3)(kysely@0.28.17)(mongodb@7.2.0)(nanostores@1.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@objectstack/plugin-org-scoping': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/plugin-security': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/rest': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/service-cluster': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/service-i18n': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/types': 7.4.1(ai@6.0.191(zod@4.4.3)) quickjs-emscripten: 0.32.0 zod: 4.4.3 optionalDependencies: - '@objectstack/driver-mongodb': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/driver-mongodb': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - '@aws-sdk/credential-providers' - '@better-auth/utils' @@ -2672,120 +2705,134 @@ snapshots: - vitest - vue - '@objectstack/service-ai@7.3.0(@ai-sdk/gateway@3.0.120(zod@4.4.3))': + '@objectstack/service-ai@7.4.1(@ai-sdk/gateway@3.0.120(zod@4.4.3))': dependencies: '@ai-sdk/provider': 3.0.10 - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/types': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/types': 7.4.1(ai@6.0.191(zod@4.4.3)) ai: 6.0.191(zod@4.4.3) zod: 4.4.3 optionalDependencies: '@ai-sdk/gateway': 3.0.120(zod@4.4.3) - '@objectstack/service-analytics@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/service-analytics@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/service-automation@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/service-automation@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/formula': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/formula': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/service-cache@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/service-cache@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/observability': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/observability': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/service-cluster@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/service-cluster@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/service-feed@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/service-external-datasource@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/service-i18n@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/service-feed@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/service-job@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/service-i18n@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/platform-objects': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) + transitivePeerDependencies: + - ai + + '@objectstack/service-job@7.4.1(ai@6.0.191(zod@4.4.3))': + dependencies: + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/platform-objects': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) croner: 10.0.1 transitivePeerDependencies: - ai - '@objectstack/service-package@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/service-messaging@7.4.1(ai@6.0.191(zod@4.4.3))': + dependencies: + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) + transitivePeerDependencies: + - ai + + '@objectstack/service-package@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/service-queue@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/service-queue@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/platform-objects': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/platform-objects': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/service-realtime@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/service-realtime@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/platform-objects': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/platform-objects': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/service-settings@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/service-settings@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: '@noble/ciphers': 2.2.0 - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/platform-objects': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/types': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/platform-objects': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/types': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/service-storage@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/service-storage@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/core': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/observability': 7.3.0(ai@6.0.191(zod@4.4.3)) - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/core': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/observability': 7.4.1(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/spec@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/spec@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: zod: 4.4.3 optionalDependencies: ai: 6.0.191(zod@4.4.3) - '@objectstack/types@7.3.0(ai@6.0.191(zod@4.4.3))': + '@objectstack/types@7.4.1(ai@6.0.191(zod@4.4.3))': dependencies: - '@objectstack/spec': 7.3.0(ai@6.0.191(zod@4.4.3)) + '@objectstack/spec': 7.4.1(ai@6.0.191(zod@4.4.3)) transitivePeerDependencies: - ai diff --git a/scripts/run-qa.mjs b/scripts/run-qa.mjs new file mode 100644 index 0000000..606defe --- /dev/null +++ b/scripts/run-qa.mjs @@ -0,0 +1,176 @@ +#!/usr/bin/env node +// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. +// +// QA scenario runner for ObjectStack templates. +// +// Runs the Quality-Protocol scenario files (qa/*.test.json, same format the +// bundled `objectstack test` uses) against a running dev server. It exists +// because the `objectstack test` adapter shipped in @objectstack/core 7.4.x +// targets the unversioned path `/api/data/`, while the 7.4.x REST +// plugin serves the versioned `/api/v1/data/` — so the bundled runner +// 404s. This runner hits the versioned path and authenticates via better-auth +// sign-up, so the scenarios actually execute end-to-end. +// +// Usage: +// node scripts/run-qa.mjs --url http://localhost:4002 [--file qa/*.test.json] +// +// Exit code is non-zero if any scenario fails. + +import { readFileSync } from 'node:fs'; +import { argv, exit } from 'node:process'; + +function arg(name, fallback) { + const i = argv.indexOf(`--${name}`); + return i >= 0 && argv[i + 1] ? argv[i + 1] : fallback; +} + +const baseUrl = (arg('url', 'http://localhost:4002')).replace(/\/$/, ''); +const apiBase = `${baseUrl}/api/v1`; +const file = arg('file', 'qa/business-workflow.test.json'); + +// ---- tiny helpers (mirror @objectstack/core TestRunner semantics) ---- +const getByPath = (obj, path) => + !path ? obj : path.split('.').reduce((c, p) => (c == null ? undefined : c[p]), obj); + +function interpolate(action, ctx) { + const s = JSON.stringify(action).replace(/\{\{([^}]+)\}\}/g, (m, p) => { + const v = getByPath(ctx, p.trim()); + if (v === undefined) return m; + return typeof v === 'string' ? v : JSON.stringify(v); + }); + try { + return JSON.parse(s); + } catch { + return action; + } +} + +function assertOne(result, a) { + const actual = getByPath(result, a.field); + const expected = a.expectedValue; + const fail = (msg) => { + throw new Error(`assertion failed: ${a.field} ${msg} (got ${JSON.stringify(actual)})`); + }; + switch (a.operator) { + case 'equals': + if (actual !== expected) fail(`expected ${JSON.stringify(expected)}`); + break; + case 'not_equals': + if (actual === expected) fail(`expected not ${JSON.stringify(expected)}`); + break; + case 'not_null': + if (actual === null || actual === undefined) fail('expected non-null'); + break; + case 'is_null': + if (actual !== null && actual !== undefined) fail('expected null'); + break; + case 'contains': + if (Array.isArray(actual)) { + if (!actual.includes(expected)) fail(`array does not contain ${JSON.stringify(expected)}`); + } else if (typeof actual === 'string') { + if (!actual.includes(String(expected))) fail(`string does not contain ${expected}`); + } else fail('not array/string'); + break; + default: + throw new Error(`unknown operator: ${a.operator}`); + } +} + +// better-auth enforces a CSRF Origin check; send the server's own origin. +let headers = { 'Content-Type': 'application/json', Origin: baseUrl }; + +async function signUp() { + const email = `qa+${Date.now()}@objectos.ai`; + const res = await fetch(`${apiBase}/auth/sign-up/email`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Origin: baseUrl }, + body: JSON.stringify({ email, password: 'qatest12345', name: 'QA Runner' }), + }); + const token = res.headers.get('set-auth-token'); + if (!res.ok || !token) { + throw new Error(`sign-up failed: HTTP ${res.status} ${await res.text()}`); + } + headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }; + return email; +} + +async function dataFetch(method, path, body) { + const res = await fetch(`${apiBase}/data/${path}`, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + const text = await res.text(); + let json; + try { + json = text ? JSON.parse(text) : {}; + } catch { + json = { raw: text }; + } + if (!res.ok) throw new Error(`HTTP ${res.status}: ${text}`); + return json; +} + +async function execute(action) { + const { type, target, payload = {} } = action; + switch (type) { + case 'create_record': + return dataFetch('POST', target, payload); + case 'update_record': { + if (!payload.id) throw new Error('update_record requires id'); + const { id, ...rest } = payload; + return dataFetch('PATCH', `${target}/${id}`, rest); + } + case 'read_record': + if (!payload.id) throw new Error('read_record requires id'); + return dataFetch('GET', `${target}/${payload.id}`); + case 'delete_record': + if (!payload.id) throw new Error('delete_record requires id'); + return dataFetch('DELETE', `${target}/${payload.id}`); + case 'query_records': + return dataFetch('POST', `${target}/query`, payload); + case 'wait': + return new Promise((r) => setTimeout(() => r({ waited: payload.duration || 0 }), payload.duration || 0)); + default: + throw new Error(`unsupported action type: ${type}`); + } +} + +async function runStep(step, ctx) { + const result = await execute(interpolate(step.action, ctx)); + if (step.capture) for (const [k, p] of Object.entries(step.capture)) ctx[k] = getByPath(result, p); + // Interpolate assertions too so expectedValue can reference captured vars. + if (step.assertions) for (const a of step.assertions) assertOne(result, interpolate(a, ctx)); + return result; +} + +async function main() { + const suite = JSON.parse(readFileSync(file, 'utf8')); + const email = await signUp(); + console.log(`\n▶ ${suite.name}`); + console.log(` server: ${apiBase} auth: ${email}\n`); + + let passed = 0; + let failed = 0; + for (const sc of suite.scenarios) { + // Seed a per-run id so fixtures can keep unique-constrained fields (e.g. + // a label name) collision-free across repeated runs on a persistent DB. + const ctx = { runId: `${Date.now()}${Math.floor(performance.now())}` }; + try { + for (const step of sc.setup || []) await runStep(step, ctx); + for (const step of sc.steps) await runStep(step, ctx); + console.log(` ✅ ${sc.id} — ${sc.name}`); + passed++; + } catch (e) { + console.log(` ❌ ${sc.id} — ${sc.name}\n ${e.message}`); + failed++; + } + } + console.log(`\n${failed === 0 ? '✅ PASS' : '❌ FAIL'}: ${passed} passed, ${failed} failed.\n`); + exit(failed === 0 ? 0 : 1); +} + +main().catch((e) => { + console.error(`\n❌ runner error: ${e.message}\n`); + exit(1); +});