@sanoma-ai/workflows 0.11.11 → 0.11.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLOUD-IDENTITY.md +12 -4
- package/KMS-SECRETS.md +3 -1
- package/README.md +6 -2
- package/dist/bundled.d.ts +2 -2
- package/dist/chats.d.ts +3 -3
- package/dist/chats.js +34 -20
- package/dist/chats.js.map +1 -1
- package/dist/client.d.ts +17 -4
- package/dist/client.js +74 -63
- package/dist/client.js.map +1 -1
- package/dist/cloud-identity.js +14 -12
- package/dist/cloud-identity.js.map +1 -1
- package/dist/collection.d.ts +2 -2
- package/dist/collection.js +53 -42
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +11 -6
- package/dist/config.js +15 -13
- package/dist/config.js.map +1 -1
- package/dist/connection.d.ts +2 -0
- package/dist/connection.js +112 -40
- package/dist/connection.js.map +1 -1
- package/dist/credentials.d.ts +1 -1
- package/dist/credentials.js +33 -29
- package/dist/credentials.js.map +1 -1
- package/dist/database.js +4 -1
- package/dist/database.js.map +1 -1
- package/dist/db/company.d.ts +1152 -0
- package/dist/db/company.js +118 -0
- package/dist/db/company.js.map +1 -0
- package/dist/db/dbos.d.ts +111 -0
- package/dist/db/dbos.js +15 -0
- package/dist/db/dbos.js.map +1 -0
- package/dist/db/identity.d.ts +367 -0
- package/dist/db/identity.js +48 -0
- package/dist/db/identity.js.map +1 -0
- package/dist/db/index.d.ts +6 -0
- package/dist/db/index.js +10 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/kms.d.ts +224 -0
- package/dist/db/kms.js +29 -0
- package/dist/db/kms.js.map +1 -0
- package/dist/db/migrate.d.ts +63 -0
- package/dist/db/migrate.js +41 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/wakeup.d.ts +271 -0
- package/dist/db/wakeup.js +34 -0
- package/dist/db/wakeup.js.map +1 -0
- package/dist/db.d.ts +42 -39
- package/dist/db.js +129 -169
- package/dist/db.js.map +1 -1
- package/dist/deadlines.js +8 -4
- package/dist/deadlines.js.map +1 -1
- package/dist/deployment-key.d.ts +4 -1
- package/dist/deployment-key.js +10 -3
- package/dist/deployment-key.js.map +1 -1
- package/dist/host-derive.d.ts +12 -3
- package/dist/host-derive.js +21 -8
- package/dist/host-derive.js.map +1 -1
- package/dist/identity-provisioning.d.ts +2 -2
- package/dist/identity-provisioning.js +75 -42
- package/dist/identity-provisioning.js.map +1 -1
- package/dist/identity-service.js +18 -15
- package/dist/identity-service.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/kms-secrets.d.ts +9 -0
- package/dist/kms-secrets.js +59 -27
- package/dist/kms-secrets.js.map +1 -1
- package/dist/ledger.d.ts +1 -1
- package/dist/ledger.js +22 -14
- package/dist/ledger.js.map +1 -1
- package/dist/op.d.ts +27 -6
- package/dist/op.js.map +1 -1
- package/dist/run-status.js +7 -3
- package/dist/run-status.js.map +1 -1
- package/dist/schedules.d.ts +1 -1
- package/dist/schedules.js +5 -7
- package/dist/schedules.js.map +1 -1
- package/dist/schema.d.ts +6 -5
- package/dist/schema.js +11 -7
- package/dist/schema.js.map +1 -1
- package/dist/shared.d.ts +6 -23
- package/dist/shared.js +6 -5
- package/dist/shared.js.map +1 -1
- package/dist/storage-provisioning.d.ts +2 -0
- package/dist/storage-provisioning.js +51 -50
- package/dist/storage-provisioning.js.map +1 -1
- package/dist/tokens.d.ts +2 -2
- package/dist/tokens.js +31 -20
- package/dist/tokens.js.map +1 -1
- package/dist/version.d.ts +4 -4
- package/dist/version.js +5 -5
- package/dist/version.js.map +1 -1
- package/dist/wakeup-service.d.ts +0 -4
- package/dist/wakeup-service.js +102 -93
- package/dist/wakeup-service.js.map +1 -1
- package/dist/worker.d.ts +7 -0
- package/dist/worker.js +26 -21
- package/dist/worker.js.map +1 -1
- package/drizzle/company/0000_tables.sql +104 -0
- package/drizzle/company/0001_wakeup_row.sql +2 -0
- package/drizzle/company/meta/0000_snapshot.json +652 -0
- package/drizzle/company/meta/0001_snapshot.json +652 -0
- package/drizzle/company/meta/_journal.json +20 -0
- package/drizzle/wakeup/0000_tables.sql +26 -0
- package/drizzle/wakeup/meta/0000_snapshot.json +200 -0
- package/drizzle/wakeup/meta/_journal.json +13 -0
- package/oxlint.json +2 -1
- package/package.json +11 -2
- package/src/bundled.ts +2 -2
- package/src/chats.ts +41 -42
- package/src/client.ts +81 -64
- package/src/cloud-identity.ts +23 -24
- package/src/collection.ts +58 -50
- package/src/config.ts +29 -19
- package/src/connection.ts +132 -102
- package/src/credentials.ts +37 -33
- package/src/database.ts +4 -2
- package/src/db/company.ts +173 -0
- package/src/db/dbos.ts +17 -0
- package/src/db/identity.ts +61 -0
- package/src/db/index.ts +10 -0
- package/src/db/kms.ts +41 -0
- package/src/db/migrate.ts +70 -0
- package/src/db/wakeup.ts +41 -0
- package/src/db.ts +151 -182
- package/src/deadlines.ts +10 -4
- package/src/deployment-key.ts +11 -3
- package/src/host-derive.ts +26 -9
- package/src/identity-provisioning.ts +85 -57
- package/src/identity-service.ts +19 -19
- package/src/index.ts +3 -0
- package/src/kms-secrets.ts +64 -56
- package/src/ledger.ts +23 -18
- package/src/op.ts +28 -6
- package/src/run-status.ts +7 -6
- package/src/schedules.ts +6 -8
- package/src/schema.ts +11 -8
- package/src/shared.ts +6 -17
- package/src/storage-provisioning.ts +47 -52
- package/src/tokens.ts +43 -33
- package/src/version.ts +5 -5
- package/src/wakeup-service.ts +117 -131
- package/src/worker.ts +38 -21
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "8ab4b417-ea98-494f-a151-5ac42f186eb5",
|
|
3
|
+
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
4
|
+
"version": "7",
|
|
5
|
+
"dialect": "postgresql",
|
|
6
|
+
"tables": {
|
|
7
|
+
"public.sanoma_wakeup_accounts": {
|
|
8
|
+
"name": "sanoma_wakeup_accounts",
|
|
9
|
+
"schema": "",
|
|
10
|
+
"columns": {
|
|
11
|
+
"id": {
|
|
12
|
+
"name": "id",
|
|
13
|
+
"type": "uuid",
|
|
14
|
+
"primaryKey": true,
|
|
15
|
+
"notNull": true
|
|
16
|
+
},
|
|
17
|
+
"token_hash": {
|
|
18
|
+
"name": "token_hash",
|
|
19
|
+
"type": "text",
|
|
20
|
+
"primaryKey": false,
|
|
21
|
+
"notNull": true
|
|
22
|
+
},
|
|
23
|
+
"label": {
|
|
24
|
+
"name": "label",
|
|
25
|
+
"type": "text",
|
|
26
|
+
"primaryKey": false,
|
|
27
|
+
"notNull": true
|
|
28
|
+
},
|
|
29
|
+
"created_at": {
|
|
30
|
+
"name": "created_at",
|
|
31
|
+
"type": "timestamp with time zone",
|
|
32
|
+
"primaryKey": false,
|
|
33
|
+
"notNull": true,
|
|
34
|
+
"default": "now()"
|
|
35
|
+
},
|
|
36
|
+
"revoked_at": {
|
|
37
|
+
"name": "revoked_at",
|
|
38
|
+
"type": "timestamp with time zone",
|
|
39
|
+
"primaryKey": false,
|
|
40
|
+
"notNull": false
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"indexes": {},
|
|
44
|
+
"foreignKeys": {},
|
|
45
|
+
"compositePrimaryKeys": {},
|
|
46
|
+
"uniqueConstraints": {
|
|
47
|
+
"sanoma_wakeup_accounts_tokenHash_unique": {
|
|
48
|
+
"name": "sanoma_wakeup_accounts_tokenHash_unique",
|
|
49
|
+
"nullsNotDistinct": false,
|
|
50
|
+
"columns": [
|
|
51
|
+
"token_hash"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"policies": {},
|
|
56
|
+
"checkConstraints": {},
|
|
57
|
+
"isRLSEnabled": false
|
|
58
|
+
},
|
|
59
|
+
"public.sanoma_wakeup_instances": {
|
|
60
|
+
"name": "sanoma_wakeup_instances",
|
|
61
|
+
"schema": "",
|
|
62
|
+
"columns": {
|
|
63
|
+
"id": {
|
|
64
|
+
"name": "id",
|
|
65
|
+
"type": "uuid",
|
|
66
|
+
"primaryKey": true,
|
|
67
|
+
"notNull": true
|
|
68
|
+
},
|
|
69
|
+
"account_id": {
|
|
70
|
+
"name": "account_id",
|
|
71
|
+
"type": "uuid",
|
|
72
|
+
"primaryKey": false,
|
|
73
|
+
"notNull": true
|
|
74
|
+
},
|
|
75
|
+
"url": {
|
|
76
|
+
"name": "url",
|
|
77
|
+
"type": "text",
|
|
78
|
+
"primaryKey": false,
|
|
79
|
+
"notNull": true
|
|
80
|
+
},
|
|
81
|
+
"token_hash": {
|
|
82
|
+
"name": "token_hash",
|
|
83
|
+
"type": "text",
|
|
84
|
+
"primaryKey": false,
|
|
85
|
+
"notNull": true
|
|
86
|
+
},
|
|
87
|
+
"at": {
|
|
88
|
+
"name": "at",
|
|
89
|
+
"type": "timestamp with time zone",
|
|
90
|
+
"primaryKey": false,
|
|
91
|
+
"notNull": false
|
|
92
|
+
},
|
|
93
|
+
"called_at": {
|
|
94
|
+
"name": "called_at",
|
|
95
|
+
"type": "timestamp with time zone",
|
|
96
|
+
"primaryKey": false,
|
|
97
|
+
"notNull": false
|
|
98
|
+
},
|
|
99
|
+
"attempts": {
|
|
100
|
+
"name": "attempts",
|
|
101
|
+
"type": "integer",
|
|
102
|
+
"primaryKey": false,
|
|
103
|
+
"notNull": true,
|
|
104
|
+
"default": 0
|
|
105
|
+
},
|
|
106
|
+
"next_attempt_at": {
|
|
107
|
+
"name": "next_attempt_at",
|
|
108
|
+
"type": "timestamp with time zone",
|
|
109
|
+
"primaryKey": false,
|
|
110
|
+
"notNull": false
|
|
111
|
+
},
|
|
112
|
+
"last_error": {
|
|
113
|
+
"name": "last_error",
|
|
114
|
+
"type": "text",
|
|
115
|
+
"primaryKey": false,
|
|
116
|
+
"notNull": false
|
|
117
|
+
},
|
|
118
|
+
"created_at": {
|
|
119
|
+
"name": "created_at",
|
|
120
|
+
"type": "timestamp with time zone",
|
|
121
|
+
"primaryKey": false,
|
|
122
|
+
"notNull": true,
|
|
123
|
+
"default": "now()"
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"indexes": {
|
|
127
|
+
"sanoma_wakeup_instances_at": {
|
|
128
|
+
"name": "sanoma_wakeup_instances_at",
|
|
129
|
+
"columns": [
|
|
130
|
+
{
|
|
131
|
+
"expression": "at",
|
|
132
|
+
"isExpression": false,
|
|
133
|
+
"asc": true,
|
|
134
|
+
"nulls": "last"
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
"isUnique": false,
|
|
138
|
+
"where": "\"sanoma_wakeup_instances\".\"at\" IS NOT NULL",
|
|
139
|
+
"concurrently": false,
|
|
140
|
+
"method": "btree",
|
|
141
|
+
"with": {}
|
|
142
|
+
},
|
|
143
|
+
"sanoma_wakeup_instances_account": {
|
|
144
|
+
"name": "sanoma_wakeup_instances_account",
|
|
145
|
+
"columns": [
|
|
146
|
+
{
|
|
147
|
+
"expression": "account_id",
|
|
148
|
+
"isExpression": false,
|
|
149
|
+
"asc": true,
|
|
150
|
+
"nulls": "last"
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
"isUnique": false,
|
|
154
|
+
"concurrently": false,
|
|
155
|
+
"method": "btree",
|
|
156
|
+
"with": {}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
"foreignKeys": {
|
|
160
|
+
"sanoma_wakeup_instances_account_id_sanoma_wakeup_accounts_id_fk": {
|
|
161
|
+
"name": "sanoma_wakeup_instances_account_id_sanoma_wakeup_accounts_id_fk",
|
|
162
|
+
"tableFrom": "sanoma_wakeup_instances",
|
|
163
|
+
"tableTo": "sanoma_wakeup_accounts",
|
|
164
|
+
"columnsFrom": [
|
|
165
|
+
"account_id"
|
|
166
|
+
],
|
|
167
|
+
"columnsTo": [
|
|
168
|
+
"id"
|
|
169
|
+
],
|
|
170
|
+
"onDelete": "no action",
|
|
171
|
+
"onUpdate": "no action"
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"compositePrimaryKeys": {},
|
|
175
|
+
"uniqueConstraints": {
|
|
176
|
+
"sanoma_wakeup_instances_tokenHash_unique": {
|
|
177
|
+
"name": "sanoma_wakeup_instances_tokenHash_unique",
|
|
178
|
+
"nullsNotDistinct": false,
|
|
179
|
+
"columns": [
|
|
180
|
+
"token_hash"
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
"policies": {},
|
|
185
|
+
"checkConstraints": {},
|
|
186
|
+
"isRLSEnabled": false
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
"enums": {},
|
|
190
|
+
"schemas": {},
|
|
191
|
+
"sequences": {},
|
|
192
|
+
"roles": {},
|
|
193
|
+
"policies": {},
|
|
194
|
+
"views": {},
|
|
195
|
+
"_meta": {
|
|
196
|
+
"columns": {},
|
|
197
|
+
"schemas": {},
|
|
198
|
+
"tables": {}
|
|
199
|
+
}
|
|
200
|
+
}
|
package/oxlint.json
CHANGED
|
@@ -103,7 +103,8 @@
|
|
|
103
103
|
"@sanoma-ai/workflows/cloud-identity",
|
|
104
104
|
"@sanoma-ai/workflows/identity-service",
|
|
105
105
|
"@sanoma-ai/workflows/kms-secrets",
|
|
106
|
-
"@sanoma-ai/workflows/wakeup-service"
|
|
106
|
+
"@sanoma-ai/workflows/wakeup-service",
|
|
107
|
+
"@sanoma-ai/workflows/db"
|
|
107
108
|
],
|
|
108
109
|
"message": "A workflow must call vendors through ctx, so the policy sees every call: import the connector itself, `@sanoma-ai/connector-<vendor>`."
|
|
109
110
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanoma-ai/workflows",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.13",
|
|
4
4
|
"description": "Business processes as TypeScript functions, run durably on DBOS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"approvals",
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
"dist",
|
|
23
23
|
"src",
|
|
24
24
|
"!src/AGENTS.md",
|
|
25
|
+
"drizzle",
|
|
26
|
+
"!drizzle/*.config.ts",
|
|
25
27
|
"oxlint.json",
|
|
26
28
|
"README.md",
|
|
27
29
|
"LICENSE",
|
|
@@ -86,6 +88,10 @@
|
|
|
86
88
|
"types": "./dist/kms-secrets.d.ts",
|
|
87
89
|
"default": "./dist/kms-secrets.js"
|
|
88
90
|
},
|
|
91
|
+
"./db": {
|
|
92
|
+
"types": "./dist/db/index.d.ts",
|
|
93
|
+
"default": "./dist/db/index.js"
|
|
94
|
+
},
|
|
89
95
|
"./identity-service": {
|
|
90
96
|
"types": "./dist/identity-service.d.ts",
|
|
91
97
|
"default": "./dist/identity-service.js"
|
|
@@ -106,6 +112,7 @@
|
|
|
106
112
|
"@google-cloud/kms": "^6.3.0",
|
|
107
113
|
"ai": "7.0.153",
|
|
108
114
|
"crc-32": "^1.2.2",
|
|
115
|
+
"drizzle-orm": "0.45.4",
|
|
109
116
|
"google-auth-library": "^11.3.2",
|
|
110
117
|
"oxc-parser": "^0.153.0",
|
|
111
118
|
"oxfmt": "^0.72.0",
|
|
@@ -115,6 +122,7 @@
|
|
|
115
122
|
},
|
|
116
123
|
"devDependencies": {
|
|
117
124
|
"@types/pg": "^8.23.1",
|
|
125
|
+
"drizzle-kit": "0.31.11",
|
|
118
126
|
"msw": "^3.0.3",
|
|
119
127
|
"srvx": "^0.11.22",
|
|
120
128
|
"zod": "^4.6.5"
|
|
@@ -129,6 +137,7 @@
|
|
|
129
137
|
"scripts": {
|
|
130
138
|
"build": "tsc -p tsconfig.build.json",
|
|
131
139
|
"clean": "rm -rf dist",
|
|
132
|
-
"watch": "tsc -p tsconfig.build.json --watch --preserveWatchOutput"
|
|
140
|
+
"watch": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
141
|
+
"generate": "drizzle-kit generate --config drizzle/company.config.ts && drizzle-kit generate --config drizzle/wakeup.config.ts"
|
|
133
142
|
}
|
|
134
143
|
}
|
package/src/bundled.ts
CHANGED
|
@@ -12,8 +12,8 @@ export interface RootSnapshot {
|
|
|
12
12
|
appName: string;
|
|
13
13
|
/** `ResolvedConfig.version`: a hash of the workflows' source, which a bundle's code is not, so a run it queues is its worker's. */
|
|
14
14
|
version: string;
|
|
15
|
-
/** `ResolvedConfig.
|
|
16
|
-
|
|
15
|
+
/** `ResolvedConfig.builtInCodes`, for the same reason. */
|
|
16
|
+
builtInCodes: Readonly<Record<string, string>>;
|
|
17
17
|
/** `readPolicyFiles`: each `.cedar` file under `policies/` (and an app's), by path from the root. */
|
|
18
18
|
policies: { file: string; text: string }[];
|
|
19
19
|
/** `readPeople`: the people, agents and groups the data files declare, with their problems. */
|
package/src/chats.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { UIMessage } from "ai";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import { asc, desc, eq, sql } from "drizzle-orm";
|
|
3
|
+
import type { Db } from "./db.ts";
|
|
4
|
+
import { chatMessages, chats } from "./db/company.ts";
|
|
4
5
|
import { SanomaError } from "./errors.ts";
|
|
5
6
|
|
|
6
7
|
// The operator agent's chats, in the config's database beside the runtime's other tables: one row
|
|
@@ -44,70 +45,68 @@ export interface Chats {
|
|
|
44
45
|
list(person: string): Promise<Chat[]>;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
interface ChatRow {
|
|
49
|
-
id: string;
|
|
50
|
-
person: string;
|
|
51
|
-
engine: string;
|
|
52
|
-
draft_id: string | null;
|
|
53
|
-
title: string;
|
|
54
|
-
created_at: Date;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const COLUMNS = "id, person, engine, draft_id, title, created_at";
|
|
58
|
-
|
|
59
|
-
const chatOf = ({ draft_id, created_at, ...row }: ChatRow): Chat => ({
|
|
48
|
+
const chatOf = ({ draftId, createdAt, ...row }: typeof chats.$inferSelect): Chat => ({
|
|
60
49
|
...row,
|
|
61
|
-
...(
|
|
62
|
-
createdAt:
|
|
50
|
+
...(draftId === null ? {} : { draftId }),
|
|
51
|
+
createdAt: createdAt.getTime(),
|
|
63
52
|
});
|
|
64
53
|
|
|
65
|
-
/** The chats' store on the
|
|
66
|
-
export function chatStore(db: () => Promise<
|
|
54
|
+
/** The chats' store on the database `db` gives, the config's database once it is prepared. */
|
|
55
|
+
export function chatStore(db: () => Promise<Db>): Chats {
|
|
67
56
|
return {
|
|
68
57
|
async open({ id, person, engine, title }) {
|
|
69
|
-
const
|
|
58
|
+
const opened = await (
|
|
70
59
|
await db()
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
60
|
+
)
|
|
61
|
+
.insert(chats)
|
|
62
|
+
.values({ id, person, engine, title })
|
|
63
|
+
.onConflictDoNothing()
|
|
64
|
+
.returning({ id: chats.id });
|
|
65
|
+
if (!opened.length) throw new SanomaError("invalid_input", `Chat ${id} exists already; open it with another id`);
|
|
76
66
|
},
|
|
77
67
|
async attach(id, draftId) {
|
|
78
|
-
await (await db()).
|
|
68
|
+
await (await db()).update(chats).set({ draftId }).where(eq(chats.id, id));
|
|
79
69
|
},
|
|
80
70
|
async upsert(id, messages) {
|
|
81
71
|
if (!messages.length) return;
|
|
82
72
|
if (messages.some((m) => !m.id)) throw new SanomaError("invalid_input", "A chat message needs an id");
|
|
83
73
|
// A new message's place is after the chat's last, in the order given.
|
|
74
|
+
const last = sql`(SELECT coalesce(max(${chatMessages.seq}), 0) FROM ${chatMessages} WHERE ${eq(chatMessages.chatId, id)})`;
|
|
84
75
|
await (
|
|
85
76
|
await db()
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
77
|
+
)
|
|
78
|
+
.insert(chatMessages)
|
|
79
|
+
.values(
|
|
80
|
+
messages.map((message, n) => ({ chatId: id, id: message.id, seq: sql<number>`${last} + ${n + 1}`, message })),
|
|
81
|
+
)
|
|
82
|
+
.onConflictDoUpdate({
|
|
83
|
+
target: [chatMessages.chatId, chatMessages.id],
|
|
84
|
+
set: { message: sql`excluded.message` },
|
|
85
|
+
});
|
|
94
86
|
},
|
|
95
87
|
async load(id) {
|
|
96
|
-
const
|
|
88
|
+
const rows = await (
|
|
97
89
|
await db()
|
|
98
|
-
)
|
|
90
|
+
)
|
|
91
|
+
.select({ message: chatMessages.message })
|
|
92
|
+
.from(chatMessages)
|
|
93
|
+
.where(eq(chatMessages.chatId, id))
|
|
94
|
+
.orderBy(asc(chatMessages.seq), asc(chatMessages.id));
|
|
99
95
|
return rows.map((row) => row.message);
|
|
100
96
|
},
|
|
101
97
|
async get(id) {
|
|
102
|
-
const
|
|
103
|
-
return
|
|
98
|
+
const [row] = await (await db()).select().from(chats).where(eq(chats.id, id));
|
|
99
|
+
return row && chatOf(row);
|
|
104
100
|
},
|
|
105
101
|
async list(person) {
|
|
106
|
-
const
|
|
102
|
+
const rows = await (
|
|
107
103
|
await db()
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
)
|
|
105
|
+
.select()
|
|
106
|
+
.from(chats)
|
|
107
|
+
.where(eq(chats.person, person))
|
|
108
|
+
.orderBy(desc(chats.createdAt), asc(chats.id))
|
|
109
|
+
.limit(50);
|
|
111
110
|
return rows.map(chatOf);
|
|
112
111
|
},
|
|
113
112
|
};
|
package/src/client.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Pool } from "pg";
|
|
1
2
|
import {
|
|
2
3
|
connectionDriver,
|
|
3
4
|
connectionTargetSchema,
|
|
@@ -13,7 +14,8 @@ import { encodeCredentials, decodeCredentials, verifyAuthentication, Authenticat
|
|
|
13
14
|
import { createHash, randomUUID } from "node:crypto";
|
|
14
15
|
import { isDeepStrictEqual } from "node:util";
|
|
15
16
|
import { type DBOSClient, Error as DBOSErrors, type WorkflowStatusString } from "@dbos-inc/dbos-sdk";
|
|
16
|
-
import
|
|
17
|
+
import { is } from "drizzle-orm";
|
|
18
|
+
import { TransactionRollbackError } from "drizzle-orm/errors";
|
|
17
19
|
import { z } from "zod";
|
|
18
20
|
import {
|
|
19
21
|
APPROVALS_EVENT,
|
|
@@ -30,7 +32,7 @@ import { type Tokens, tokenStore } from "./tokens.ts";
|
|
|
30
32
|
import { collectionsDriver } from "./collection.ts";
|
|
31
33
|
import { type ResolvedConfig, resolveConfig, type SanomaConfig } from "./config.ts";
|
|
32
34
|
import { configuredSecrets } from "./credentials.ts";
|
|
33
|
-
import { dbosClient,
|
|
35
|
+
import { dbosClient, lazyDb, type LazyDb, spendInviteRequest } from "./db.ts";
|
|
34
36
|
import { type CredentialStatus, credentialsOf } from "./env.ts";
|
|
35
37
|
import { type ApprovalState, Principal, type WorkflowDefinition } from "./define.ts";
|
|
36
38
|
import { APPLY_WORKFLOW, type ApplyInput, type ApplyReport } from "./apply.ts";
|
|
@@ -263,6 +265,12 @@ export interface ClientOptions {
|
|
|
263
265
|
* `origin`) and fetches a draft's new commits into. Without one, `remoteEvent` does nothing.
|
|
264
266
|
*/
|
|
265
267
|
source?: Source;
|
|
268
|
+
/**
|
|
269
|
+
* The version each run it starts is queued for, so only a worker of that code runs it: the app
|
|
270
|
+
* bundle's (its snapshot's), which is live beside a worker of another version while a deploy
|
|
271
|
+
* rolls. Without it a run is queued with none, which DBOS gives the app's latest version.
|
|
272
|
+
*/
|
|
273
|
+
appVersion?: string;
|
|
266
274
|
}
|
|
267
275
|
|
|
268
276
|
/** Talks to the runtime from another process (the app, a script), through the shared Postgres. */
|
|
@@ -273,7 +281,7 @@ export class SanomaClient {
|
|
|
273
281
|
/** The config's own ledger store, or a Postgres one made for this client, which `close` then closes. */
|
|
274
282
|
private readonly audit: OwnedLedger;
|
|
275
283
|
/** On the config's database, for the runtime's tables (the credentials, the chats): opened on first use. */
|
|
276
|
-
private readonly
|
|
284
|
+
private readonly database: LazyDb;
|
|
277
285
|
/** What a direct call's driver reads secrets from, as a worker's live driver does. */
|
|
278
286
|
private readonly storedSecrets: SecretAccess;
|
|
279
287
|
/** The operator agent's chats: their rows and their transcripts, in the config's database. */
|
|
@@ -293,10 +301,10 @@ export class SanomaClient {
|
|
|
293
301
|
this.drivers = drivers;
|
|
294
302
|
this.options = options;
|
|
295
303
|
this.audit = ownedLedger(config);
|
|
296
|
-
this.
|
|
297
|
-
this.storedSecrets = configuredSecrets(config.secrets, () => this.db());
|
|
298
|
-
this.chats = chatStore(() => this.db());
|
|
299
|
-
this.tokens = tokenStore(() => this.db());
|
|
304
|
+
this.database = lazyDb(config.databaseUrl, "client");
|
|
305
|
+
this.storedSecrets = configuredSecrets(config.secrets, () => this.database.db());
|
|
306
|
+
this.chats = chatStore(() => this.database.db());
|
|
307
|
+
this.tokens = tokenStore(() => this.database.db());
|
|
300
308
|
}
|
|
301
309
|
|
|
302
310
|
/**
|
|
@@ -321,7 +329,8 @@ export class SanomaClient {
|
|
|
321
329
|
* Checks the input against the workflow's schema, then queues a run for the worker. The run
|
|
322
330
|
* gets the input as sent, not as the schema parsed it: the worker parses it once, so a schema
|
|
323
331
|
* with a `.transform` or a default sees the caller's value, and `run.started` records it.
|
|
324
|
-
* `workflow` is a definition, or the name of one in the config's `workflows`.
|
|
332
|
+
* `workflow` is a definition, or the name of one in the config's `workflows`. The run is queued
|
|
333
|
+
* for the client's `appVersion` when it has one.
|
|
325
334
|
*/
|
|
326
335
|
async start<S extends z.ZodType>(
|
|
327
336
|
workflow: WorkflowDefinition<any, S> | string,
|
|
@@ -363,11 +372,11 @@ export class SanomaClient {
|
|
|
363
372
|
const args: RunArgs = { input, startedBy, ...(sandbox === undefined ? {} : { sandbox }), ...(hold && { hold }) };
|
|
364
373
|
// Kept on the run's status row too, so a listing tells sandbox runs apart without its input,
|
|
365
374
|
// and says whom an agent's run is for (DBOS keeps the principal's id and groups itself); a
|
|
366
|
-
//
|
|
375
|
+
// built-in run's code, so a worker on another version with the same takes it over.
|
|
367
376
|
const attributes = {
|
|
368
377
|
sandbox,
|
|
369
378
|
onBehalfOf: startedBy.onBehalfOf,
|
|
370
|
-
|
|
379
|
+
builtInCode: this.config.builtInCodes[workflow.name],
|
|
371
380
|
};
|
|
372
381
|
this.options.onWrite?.();
|
|
373
382
|
// An id DBOS has already returns that run, unless it is another workflow's, which DBOS refuses.
|
|
@@ -378,6 +387,7 @@ export class SanomaClient {
|
|
|
378
387
|
workflowName: workflow.name,
|
|
379
388
|
workflowID: runId,
|
|
380
389
|
applicationName: this.config.appName,
|
|
390
|
+
...(this.options.appVersion !== undefined && { appVersion: this.options.appVersion }),
|
|
381
391
|
authenticatedUser: startedBy.id,
|
|
382
392
|
authenticatedRoles: startedBy.groups ?? [],
|
|
383
393
|
...(Object.values(attributes).some((v) => v !== undefined) && { attributes }),
|
|
@@ -525,64 +535,63 @@ export class SanomaClient {
|
|
|
525
535
|
const { dataStore, collections, ledger } = this.config;
|
|
526
536
|
const connectionMutation = op.id.startsWith("sanoma.connection.");
|
|
527
537
|
const transactionStore = connectionMutation
|
|
528
|
-
? this.config.
|
|
538
|
+
? this.config.connectionStore
|
|
529
539
|
: op.vendor === DATA_VENDOR
|
|
530
540
|
? dataStore
|
|
531
541
|
: undefined;
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
type: "op.called",
|
|
554
|
-
op: op.id,
|
|
555
|
-
effect: op.effect,
|
|
556
|
-
input: logged,
|
|
557
|
-
decision,
|
|
558
|
-
...(context.connection && { connection: context.connection }),
|
|
559
|
-
...outcome,
|
|
560
|
-
durationMs: Date.now() - at,
|
|
561
|
-
});
|
|
562
|
-
let output: unknown;
|
|
542
|
+
const at = Date.now();
|
|
543
|
+
const record = (outcome: { output: unknown } | { error: ErrorInfo }): LedgerRecord => ({
|
|
544
|
+
v: 1,
|
|
545
|
+
app: this.config.appName,
|
|
546
|
+
id: `${runId}:op.called:0`,
|
|
547
|
+
runId,
|
|
548
|
+
seq: 0,
|
|
549
|
+
at,
|
|
550
|
+
actor,
|
|
551
|
+
workflow: CALL_WORKFLOW,
|
|
552
|
+
type: "op.called",
|
|
553
|
+
op: op.id,
|
|
554
|
+
effect: op.effect,
|
|
555
|
+
input: logged,
|
|
556
|
+
decision,
|
|
557
|
+
...(context.connection && { connection: context.connection }),
|
|
558
|
+
...outcome,
|
|
559
|
+
durationMs: Date.now() - at,
|
|
560
|
+
});
|
|
561
|
+
/** Runs the call through `fn`; a failure is recorded on the store's own connection, outside any transaction. */
|
|
562
|
+
const output = async (fn: typeof call.fn): Promise<unknown> => {
|
|
563
563
|
try {
|
|
564
|
-
|
|
564
|
+
return await outputOf(op, () => fn(parsed, context));
|
|
565
565
|
} catch (err) {
|
|
566
|
-
// Recorded on the store's own connection; the transaction is rolled back below.
|
|
567
566
|
await this.audit.store.append(record({ error: errorInfo(err) }));
|
|
568
567
|
throw err;
|
|
569
568
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
569
|
+
};
|
|
570
|
+
if (ledger || !transactionStore) {
|
|
571
|
+
const result = await output(call.fn);
|
|
572
|
+
await this.audit.store.append(record({ output: recorded(op, result) }));
|
|
573
|
+
return result;
|
|
574
|
+
}
|
|
575
|
+
// The write and its record in one transaction: the metadata or collection driver runs on the
|
|
576
|
+
// transaction's connection (neither has an env guard), and a record another write of the id
|
|
577
|
+
// was first with rolls the write back, for the recorded output to stand.
|
|
578
|
+
const store = await transactionStore.db();
|
|
579
|
+
try {
|
|
580
|
+
return await store.transaction(async (tx) => {
|
|
581
|
+
const fn = (
|
|
582
|
+
connectionMutation
|
|
583
|
+
? connectionDriver(this.config.connections.onDatabase(tx))
|
|
584
|
+
: collectionsDriver([collections.get(op.resource)!], { db: async () => tx })
|
|
585
|
+
).ops[`${op.resource}.${op.name}`]!;
|
|
586
|
+
const result = await output(fn);
|
|
587
|
+
if (await insertRecord(tx, record({ output: recorded(op, result) }))) return result;
|
|
588
|
+
tx.rollback();
|
|
589
|
+
});
|
|
581
590
|
} catch (err) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
591
|
+
if (!is(err, TransactionRollbackError)) throw err;
|
|
592
|
+
const first = await this.recordedCall(runId, op.id, logged, context.connection);
|
|
593
|
+
if (!first) throw err;
|
|
594
|
+
return first.output;
|
|
586
595
|
}
|
|
587
596
|
}
|
|
588
597
|
|
|
@@ -675,6 +684,14 @@ export class SanomaClient {
|
|
|
675
684
|
);
|
|
676
685
|
}
|
|
677
686
|
|
|
687
|
+
/**
|
|
688
|
+
* Spends a sealed invite request's `nonce` (`openInvite`), good until `expiresAt`: true the first
|
|
689
|
+
* time, false when it was spent already, in this process or another.
|
|
690
|
+
*/
|
|
691
|
+
async spendInviteRequest(nonce: string, expiresAt: Date): Promise<boolean> {
|
|
692
|
+
return spendInviteRequest(await this.database.db(), nonce, expiresAt);
|
|
693
|
+
}
|
|
694
|
+
|
|
678
695
|
/** When each of `people` last signed in to the staff login, and how, by id (`recordSignIn`); one who never did is left out. */
|
|
679
696
|
lastSignIns(people: readonly string[]): Promise<Record<string, { at: number; method: SignInMethod }>> {
|
|
680
697
|
return lastSignIns(this.audit.store, people);
|
|
@@ -1479,10 +1496,10 @@ export class SanomaClient {
|
|
|
1479
1496
|
this.config.connections.close(),
|
|
1480
1497
|
this.config.identityProvisioning.close(),
|
|
1481
1498
|
this.dbos.destroy(),
|
|
1482
|
-
this.
|
|
1499
|
+
this.database.close(),
|
|
1483
1500
|
this.audit.close(),
|
|
1484
1501
|
this.config.dataStore?.close(),
|
|
1485
|
-
this.config.
|
|
1502
|
+
this.config.connectionStore.close(),
|
|
1486
1503
|
]);
|
|
1487
1504
|
}
|
|
1488
1505
|
|
|
@@ -1506,9 +1523,9 @@ export class SanomaClient {
|
|
|
1506
1523
|
});
|
|
1507
1524
|
}
|
|
1508
1525
|
|
|
1509
|
-
/** The pool the
|
|
1510
|
-
|
|
1511
|
-
return this.pool
|
|
1526
|
+
/** The pool the client reads and writes with, once the database is prepared: what a login runs on (`LoginContext.database`). */
|
|
1527
|
+
pool(): Promise<Pool> {
|
|
1528
|
+
return this.database.pool();
|
|
1512
1529
|
}
|
|
1513
1530
|
|
|
1514
1531
|
/**
|