@ganju/utils 0.0.15 → 0.0.16
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/dist/constants.d.ts +23 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +95 -8
- package/dist/data.d.ts +9 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +78 -0
- package/dist/dataApi.d.ts +511 -0
- package/dist/dataApi.d.ts.map +1 -0
- package/dist/dataApi.js +355 -0
- package/dist/dataCompiler.d.ts +68 -0
- package/dist/dataCompiler.d.ts.map +1 -0
- package/dist/dataCompiler.js +726 -0
- package/dist/dataDdl.d.ts +29 -0
- package/dist/dataDdl.d.ts.map +1 -0
- package/dist/dataDdl.js +354 -0
- package/dist/dataEngine.d.ts +143 -0
- package/dist/dataEngine.d.ts.map +1 -0
- package/dist/dataEngine.js +2022 -0
- package/dist/dataIdentifiers.d.ts +8 -0
- package/dist/dataIdentifiers.d.ts.map +1 -0
- package/dist/dataIdentifiers.js +55 -0
- package/dist/dataImport.d.ts +77 -0
- package/dist/dataImport.d.ts.map +1 -0
- package/dist/dataImport.js +450 -0
- package/dist/dataRpc.d.ts +131 -0
- package/dist/dataRpc.d.ts.map +1 -0
- package/dist/dataRpc.js +2 -0
- package/dist/dataTypes.d.ts +157 -0
- package/dist/dataTypes.d.ts.map +1 -0
- package/dist/dataTypes.js +160 -0
- package/dist/index.d.ts +308 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/oauth.d.ts +14 -0
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +42 -2
- package/dist/resourceRerank.d.ts +3 -2
- package/dist/resourceRerank.d.ts.map +1 -1
- package/dist/resourceRerank.js +5 -2
- package/dist/schema.d.ts +285 -4
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +203 -0
- package/package.json +17 -2
- package/src/constants.ts +108 -8
- package/src/data.ts +74 -0
- package/src/dataApi.ts +428 -0
- package/src/dataCompiler.ts +948 -0
- package/src/dataDdl.ts +419 -0
- package/src/dataEngine.ts +2753 -0
- package/src/dataIdentifiers.ts +63 -0
- package/src/dataImport.ts +554 -0
- package/src/dataRpc.ts +121 -0
- package/src/dataTypes.ts +312 -0
- package/src/index.ts +4 -0
- package/src/oauth.ts +42 -1
- package/src/resourceRerank.ts +5 -2
- package/src/schema.ts +246 -0
package/src/dataRpc.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// The methods a project's database object answers over RPC. apps/data's
|
|
2
|
+
// ProjectDatabase implements it; apps/api, apps/mcp and apps/tool-broker call
|
|
3
|
+
// it through databaseStub. Every method resolves to a DataResult.
|
|
4
|
+
import type {
|
|
5
|
+
DataCaller,
|
|
6
|
+
DataDeleteImpact,
|
|
7
|
+
DataResult,
|
|
8
|
+
DataSchema,
|
|
9
|
+
DataTable,
|
|
10
|
+
DataTableCopy
|
|
11
|
+
} from './dataTypes';
|
|
12
|
+
|
|
13
|
+
export type ExportFormat = 'jsonl' | 'csv';
|
|
14
|
+
|
|
15
|
+
export interface ExportFile {
|
|
16
|
+
// null for schema.json
|
|
17
|
+
table: string | null;
|
|
18
|
+
key: string;
|
|
19
|
+
rows: number;
|
|
20
|
+
bytes: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ExportState {
|
|
24
|
+
id: string;
|
|
25
|
+
format: ExportFormat;
|
|
26
|
+
status: 'running' | 'done' | 'failed';
|
|
27
|
+
startedAt: string;
|
|
28
|
+
finishedAt: string | null;
|
|
29
|
+
actor: string;
|
|
30
|
+
tables: string[];
|
|
31
|
+
tableIndex: number;
|
|
32
|
+
afterId: number;
|
|
33
|
+
upload: {
|
|
34
|
+
key: string;
|
|
35
|
+
uploadId: string;
|
|
36
|
+
parts: { partNumber: number; etag: string }[];
|
|
37
|
+
rows: number;
|
|
38
|
+
bytes: number;
|
|
39
|
+
// Bytes read but not yet uploaded, kept in R2 between steps: every part
|
|
40
|
+
// but the last must be exactly the same size.
|
|
41
|
+
carryKey: string | null;
|
|
42
|
+
} | null;
|
|
43
|
+
files: ExportFile[];
|
|
44
|
+
error: string | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DatabaseInfo {
|
|
48
|
+
artifactId: string | null;
|
|
49
|
+
name: string | null;
|
|
50
|
+
initializedAt: string | null;
|
|
51
|
+
deletedAt: string | null;
|
|
52
|
+
firstWriteAt: string | null;
|
|
53
|
+
schemaVersion: number;
|
|
54
|
+
size: number;
|
|
55
|
+
tables: { name: string; label: string | null; columns: number }[];
|
|
56
|
+
// A column change copying a large table in the background: table changes,
|
|
57
|
+
// and writes that would change its rows, wait while it runs.
|
|
58
|
+
copy: DataTableCopy | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RestoreResult {
|
|
62
|
+
bookmark: string;
|
|
63
|
+
undoBookmark: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type R<T> = Promise<DataResult<T>>;
|
|
67
|
+
|
|
68
|
+
export interface ProjectDatabaseRpc {
|
|
69
|
+
init(args: { artifactId: string; name: string }): R<DatabaseInfo>;
|
|
70
|
+
info(): R<DatabaseInfo>;
|
|
71
|
+
rename(args: { name: string }): R<DatabaseInfo>;
|
|
72
|
+
size(): R<{ bytes: number }>;
|
|
73
|
+
markDeleted(
|
|
74
|
+
caller: DataCaller
|
|
75
|
+
): R<{ deletedAt: string | null; bookmark: string }>;
|
|
76
|
+
undelete(caller: DataCaller): R<DatabaseInfo>;
|
|
77
|
+
purge(): R<{ purged: true; bookmark: string }>;
|
|
78
|
+
currentBookmark(): R<{ bookmark: string }>;
|
|
79
|
+
restoreTo(caller: DataCaller, args: { timestamp: number }): R<RestoreResult>;
|
|
80
|
+
restoreToBookmark(args: { bookmark: string }): R<RestoreResult>;
|
|
81
|
+
schemaRead(): R<DataSchema>;
|
|
82
|
+
createTable(caller: DataCaller, input: unknown): R<DataTable>;
|
|
83
|
+
updateTable(caller: DataCaller, table: string, patch: unknown): R<DataTable>;
|
|
84
|
+
dropTable(
|
|
85
|
+
caller: DataCaller,
|
|
86
|
+
table: string
|
|
87
|
+
): R<{ dropped: string; bookmark: string }>;
|
|
88
|
+
addColumn(caller: DataCaller, table: string, input: unknown): R<DataTable>;
|
|
89
|
+
updateColumn(
|
|
90
|
+
caller: DataCaller,
|
|
91
|
+
table: string,
|
|
92
|
+
column: string,
|
|
93
|
+
patch: unknown
|
|
94
|
+
): R<{ table: DataTable; bookmark: string | null; copy: DataTableCopy | null }>;
|
|
95
|
+
dropColumn(
|
|
96
|
+
caller: DataCaller,
|
|
97
|
+
table: string,
|
|
98
|
+
column: string
|
|
99
|
+
): R<{ table: DataTable; bookmark: string; copy: DataTableCopy | null }>;
|
|
100
|
+
// Forgets a failed background copy once the owner has seen why.
|
|
101
|
+
dismissCopy(caller: DataCaller): R<{ copy: null }>;
|
|
102
|
+
describe(caller: DataCaller, args?: unknown): R<unknown>;
|
|
103
|
+
query(caller: DataCaller, args: unknown): R<unknown>;
|
|
104
|
+
write(
|
|
105
|
+
caller: DataCaller,
|
|
106
|
+
args: unknown
|
|
107
|
+
): R<{ ok: true; results: unknown[] }>;
|
|
108
|
+
deleteImpact(
|
|
109
|
+
caller: DataCaller,
|
|
110
|
+
args: { table: string; ids?: number[] }
|
|
111
|
+
): R<DataDeleteImpact>;
|
|
112
|
+
history(
|
|
113
|
+
caller: DataCaller,
|
|
114
|
+
args: { table: string; id: number; limit?: number; cursor?: number }
|
|
115
|
+
): R<unknown>;
|
|
116
|
+
exportStart(
|
|
117
|
+
caller: DataCaller,
|
|
118
|
+
args?: { format?: ExportFormat }
|
|
119
|
+
): R<{ exportId: string; status: ExportState['status'] }>;
|
|
120
|
+
exportStatus(args?: { exportId?: string }): R<ExportState>;
|
|
121
|
+
}
|
package/src/dataTypes.ts
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// The shapes and limits shared by a project's database object (apps/data),
|
|
2
|
+
// the workers that call it, and the tests. Pure: nothing here touches SQLite.
|
|
3
|
+
|
|
4
|
+
export const DATA_COLUMN_TYPES = [
|
|
5
|
+
'text',
|
|
6
|
+
'long_text',
|
|
7
|
+
'integer',
|
|
8
|
+
'decimal',
|
|
9
|
+
'boolean',
|
|
10
|
+
'date',
|
|
11
|
+
'datetime',
|
|
12
|
+
'enum',
|
|
13
|
+
'json',
|
|
14
|
+
'email',
|
|
15
|
+
'url',
|
|
16
|
+
'phone',
|
|
17
|
+
'relation',
|
|
18
|
+
'file'
|
|
19
|
+
] as const;
|
|
20
|
+
export type DataColumnType = (typeof DATA_COLUMN_TYPES)[number];
|
|
21
|
+
|
|
22
|
+
// What the assistant may do with a table when the caller is restricted (a
|
|
23
|
+
// channel bot). Owners, the dashboard, the CLI and functions see every table.
|
|
24
|
+
export const DATA_ACCESS_LEVELS = ['none', 'read', 'add', 'full'] as const;
|
|
25
|
+
export type DataAccess = (typeof DATA_ACCESS_LEVELS)[number];
|
|
26
|
+
export const DATA_ACCESS_RANK: Record<DataAccess, number> = {
|
|
27
|
+
none: 0,
|
|
28
|
+
read: 1,
|
|
29
|
+
add: 2,
|
|
30
|
+
full: 3
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const DATA_ON_DELETE = ['restrict', 'cascade', 'set_null'] as const;
|
|
34
|
+
export type DataOnDelete = (typeof DATA_ON_DELETE)[number];
|
|
35
|
+
|
|
36
|
+
export type DataDefaultValue = string | number | boolean | null;
|
|
37
|
+
|
|
38
|
+
// A column as the owner defines it: the dashboard's form, `ganju.data.json`
|
|
39
|
+
// and the table-create body all use this shape.
|
|
40
|
+
export interface DataColumnInput {
|
|
41
|
+
name: string;
|
|
42
|
+
type: DataColumnType;
|
|
43
|
+
label?: string | null;
|
|
44
|
+
description?: string | null;
|
|
45
|
+
required?: boolean;
|
|
46
|
+
unique?: boolean;
|
|
47
|
+
filterable?: boolean;
|
|
48
|
+
searchable?: boolean;
|
|
49
|
+
personal?: boolean;
|
|
50
|
+
// enum
|
|
51
|
+
values?: string[];
|
|
52
|
+
// relation: the table it points at, and what deleting that row does
|
|
53
|
+
table?: string;
|
|
54
|
+
onDelete?: DataOnDelete;
|
|
55
|
+
// integer / decimal: the lowest value the column may hold
|
|
56
|
+
min?: number | null;
|
|
57
|
+
default?: DataDefaultValue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface DataTableInput {
|
|
61
|
+
name: string;
|
|
62
|
+
label?: string | null;
|
|
63
|
+
description?: string | null;
|
|
64
|
+
displayColumn?: string | null;
|
|
65
|
+
access?: DataAccess;
|
|
66
|
+
customerScoped?: boolean;
|
|
67
|
+
history?: boolean;
|
|
68
|
+
columns: DataColumnInput[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A column as the database object stores it: the input plus its stable id,
|
|
72
|
+
// which names its indexes and constraints so a rename never orphans them.
|
|
73
|
+
export interface DataColumn {
|
|
74
|
+
id: number;
|
|
75
|
+
name: string;
|
|
76
|
+
type: DataColumnType;
|
|
77
|
+
label: string | null;
|
|
78
|
+
description: string | null;
|
|
79
|
+
required: boolean;
|
|
80
|
+
unique: boolean;
|
|
81
|
+
filterable: boolean;
|
|
82
|
+
searchable: boolean;
|
|
83
|
+
personal: boolean;
|
|
84
|
+
values: string[] | null;
|
|
85
|
+
table: string | null;
|
|
86
|
+
onDelete: DataOnDelete | null;
|
|
87
|
+
min: number | null;
|
|
88
|
+
default: DataDefaultValue | undefined;
|
|
89
|
+
position: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface DataTable {
|
|
93
|
+
id: number;
|
|
94
|
+
name: string;
|
|
95
|
+
label: string | null;
|
|
96
|
+
description: string | null;
|
|
97
|
+
displayColumn: string | null;
|
|
98
|
+
access: DataAccess;
|
|
99
|
+
customerScoped: boolean;
|
|
100
|
+
history: boolean;
|
|
101
|
+
position: number;
|
|
102
|
+
columns: DataColumn[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface DataSchema {
|
|
106
|
+
tables: DataTable[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Who is calling and under which rules. `actor` is recorded on every row a
|
|
110
|
+
// call writes; the storage layer refuses a write without one.
|
|
111
|
+
export interface DataCaller {
|
|
112
|
+
// user:<id> · cli:<tokenId> · mcp:<userId> · channel:<participantId> ·
|
|
113
|
+
// function:<toolName> · trigger:<id> · system:<job>
|
|
114
|
+
actor: string;
|
|
115
|
+
// Apply each table's assistant access. Set for channel bots only.
|
|
116
|
+
restricted?: boolean;
|
|
117
|
+
// Customer-scoped tables: fill and filter `participant_id` with this.
|
|
118
|
+
participantId?: string;
|
|
119
|
+
// Datetimes come back in this zone, with the offset.
|
|
120
|
+
timeZone?: string;
|
|
121
|
+
limits?: DataCallerLimits;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface DataCallerLimits {
|
|
125
|
+
// Writes that add data stop at this size (the plan's ceiling).
|
|
126
|
+
maxBytes?: number | null;
|
|
127
|
+
maxTables?: number | null;
|
|
128
|
+
// The organization left the paid plan: reads and exports only.
|
|
129
|
+
readOnly?: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface DataUsage {
|
|
133
|
+
rowsRead: number;
|
|
134
|
+
rowsWritten: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const DATA_ERROR_CODES = [
|
|
138
|
+
'invalid',
|
|
139
|
+
'not_found',
|
|
140
|
+
'forbidden',
|
|
141
|
+
'conflict',
|
|
142
|
+
'constraint',
|
|
143
|
+
'guard',
|
|
144
|
+
'too_large',
|
|
145
|
+
'full',
|
|
146
|
+
'read_only',
|
|
147
|
+
'deleted',
|
|
148
|
+
'not_initialised',
|
|
149
|
+
'restore',
|
|
150
|
+
'internal'
|
|
151
|
+
] as const;
|
|
152
|
+
export type DataErrorCode = (typeof DATA_ERROR_CODES)[number];
|
|
153
|
+
|
|
154
|
+
// The one error the database object returns. Its message is written for the
|
|
155
|
+
// person or model that made the call, never raw SQLite text.
|
|
156
|
+
export class DataError extends Error {
|
|
157
|
+
readonly code: DataErrorCode;
|
|
158
|
+
readonly details: Record<string, unknown> | undefined;
|
|
159
|
+
|
|
160
|
+
constructor(
|
|
161
|
+
message: string,
|
|
162
|
+
code: DataErrorCode = 'invalid',
|
|
163
|
+
details?: Record<string, unknown>
|
|
164
|
+
) {
|
|
165
|
+
super(message);
|
|
166
|
+
this.name = 'DataError';
|
|
167
|
+
this.code = code;
|
|
168
|
+
this.details = details;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// What the API answers with; its error handler reads both.
|
|
172
|
+
get status(): 400 | 403 | 404 | 409 | 500 {
|
|
173
|
+
return DATA_ERROR_STATUS[this.code];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
toBody(): Record<string, unknown> {
|
|
177
|
+
return { code: this.code, ...(this.details ? { details: this.details } : {}) };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const DATA_ERROR_STATUS: Record<DataErrorCode, 400 | 403 | 404 | 409 | 500> = {
|
|
182
|
+
invalid: 400,
|
|
183
|
+
not_found: 404,
|
|
184
|
+
forbidden: 403,
|
|
185
|
+
conflict: 409,
|
|
186
|
+
constraint: 409,
|
|
187
|
+
guard: 409,
|
|
188
|
+
too_large: 400,
|
|
189
|
+
full: 409,
|
|
190
|
+
read_only: 403,
|
|
191
|
+
deleted: 409,
|
|
192
|
+
not_initialised: 404,
|
|
193
|
+
restore: 400,
|
|
194
|
+
internal: 500
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const isDataError = (error: unknown): error is DataError =>
|
|
198
|
+
error instanceof DataError;
|
|
199
|
+
|
|
200
|
+
// What every database object method returns. Errors travel as values, not
|
|
201
|
+
// throws: an RPC exception keeps only its message, and the caller needs the
|
|
202
|
+
// code and the rows read before it.
|
|
203
|
+
export type DataResult<T> =
|
|
204
|
+
| { ok: true; value: T; usage: DataUsage }
|
|
205
|
+
| {
|
|
206
|
+
ok: false;
|
|
207
|
+
error: {
|
|
208
|
+
code: DataErrorCode;
|
|
209
|
+
message: string;
|
|
210
|
+
details?: Record<string, unknown>;
|
|
211
|
+
};
|
|
212
|
+
usage: DataUsage;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Limits. SQLite in a Durable Object allows 100 columns per table, 100 bound
|
|
216
|
+
// parameters per statement and 100 KB per statement; the rest are ours.
|
|
217
|
+
export const DATA_MAX_PARAMS = 100;
|
|
218
|
+
export const DATA_MAX_STATEMENT_BYTES = 90_000;
|
|
219
|
+
// 100 minus id, created_at, updated_at, version, created_by, updated_by and
|
|
220
|
+
// participant_id.
|
|
221
|
+
export const DATA_MAX_COLUMNS = 93;
|
|
222
|
+
export const DATA_MAX_TABLES = 50;
|
|
223
|
+
export const DATA_DEFAULT_PAGE = 20;
|
|
224
|
+
export const DATA_MAX_PAGE = 500;
|
|
225
|
+
export const DATA_MAX_RESULT_BYTES = 1024 * 1024;
|
|
226
|
+
// Past this many rows, an aggregate or a sort on an unindexed column needs a
|
|
227
|
+
// filter on an indexed one: the object serves one request at a time.
|
|
228
|
+
export const DATA_LARGE_TABLE_ROWS = 200_000;
|
|
229
|
+
export const DATA_MAX_WRITE_OPERATIONS = 50;
|
|
230
|
+
export const DATA_MAX_INSERT_ROWS = 500;
|
|
231
|
+
export const DATA_MAX_ROWS_PER_WRITE = 5_000;
|
|
232
|
+
export const DATA_MAX_VALUE_BYTES = 1_000_000;
|
|
233
|
+
export const DATA_MAX_DESCRIPTION = 300;
|
|
234
|
+
export const DATA_MAX_LABEL = 100;
|
|
235
|
+
export const DATA_MAX_ENUM_VALUES = 100;
|
|
236
|
+
export const DATA_MAX_IN_VALUES = 50;
|
|
237
|
+
export const DATA_MAX_SORT_KEYS = 3;
|
|
238
|
+
export const DATA_MAX_AGGREGATES = 5;
|
|
239
|
+
export const DATA_MAX_GROUP_BY = 3;
|
|
240
|
+
export const DATA_MAX_GROUPS = 100;
|
|
241
|
+
// A type change copies the table in one request up to this size. A larger
|
|
242
|
+
// table is copied in the background, this many rows a step (tens of
|
|
243
|
+
// milliseconds, so other requests are served in between), and writes that
|
|
244
|
+
// would change its rows wait until it is done.
|
|
245
|
+
export const DATA_REBUILD_MAX_ROWS = 100_000;
|
|
246
|
+
export const DATA_COPY_STEP_ROWS = 5_000;
|
|
247
|
+
// A delete preview follows cascades through up to this many rows; past it,
|
|
248
|
+
// its counts are lower bounds.
|
|
249
|
+
export const DATA_IMPACT_MAX_ROWS = 200_000;
|
|
250
|
+
export const DATA_IMPACT_MAX_IDS = DATA_MAX_ROWS_PER_WRITE;
|
|
251
|
+
export const DATA_HISTORY_DAYS = 90;
|
|
252
|
+
|
|
253
|
+
// What deleting rows does elsewhere: one entry per relation column that points
|
|
254
|
+
// at a row the delete removes, cascades followed all the way down. `depth` is
|
|
255
|
+
// how many cascades away from the deleted rows the pointed-at table is.
|
|
256
|
+
export interface DataDeleteImpact {
|
|
257
|
+
table: string;
|
|
258
|
+
rows: number;
|
|
259
|
+
effects: {
|
|
260
|
+
table: string;
|
|
261
|
+
column: string;
|
|
262
|
+
onDelete: DataOnDelete;
|
|
263
|
+
rows: number;
|
|
264
|
+
depth: number;
|
|
265
|
+
}[];
|
|
266
|
+
// A restrict would refuse the delete.
|
|
267
|
+
blocked: boolean;
|
|
268
|
+
// Past DATA_IMPACT_MAX_ROWS: every count is at least what it says.
|
|
269
|
+
truncated: boolean;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// A table copy running in the background, as callers see it.
|
|
273
|
+
export interface DataTableCopy {
|
|
274
|
+
table: string;
|
|
275
|
+
// The column whose change or drop started it.
|
|
276
|
+
column: string;
|
|
277
|
+
kind: 'change' | 'drop';
|
|
278
|
+
rows: number;
|
|
279
|
+
copied: number;
|
|
280
|
+
startedAt: string;
|
|
281
|
+
actor: string;
|
|
282
|
+
status: 'running' | 'failed';
|
|
283
|
+
error: string | null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// The table as the owner would write it again: what `ganju.data.json` and an
|
|
287
|
+
// export's schema.json hold.
|
|
288
|
+
export const toTableInput = (table: DataTable): DataTableInput => ({
|
|
289
|
+
name: table.name,
|
|
290
|
+
label: table.label,
|
|
291
|
+
description: table.description,
|
|
292
|
+
displayColumn: table.displayColumn,
|
|
293
|
+
access: table.access,
|
|
294
|
+
customerScoped: table.customerScoped,
|
|
295
|
+
history: table.history,
|
|
296
|
+
columns: table.columns.map(column => {
|
|
297
|
+
const out: DataColumnInput = { name: column.name, type: column.type };
|
|
298
|
+
if (column.label) out.label = column.label;
|
|
299
|
+
if (column.description) out.description = column.description;
|
|
300
|
+
if (column.required) out.required = true;
|
|
301
|
+
if (column.unique) out.unique = true;
|
|
302
|
+
if (column.filterable) out.filterable = true;
|
|
303
|
+
if (column.searchable) out.searchable = true;
|
|
304
|
+
if (column.personal) out.personal = true;
|
|
305
|
+
if (column.values) out.values = column.values;
|
|
306
|
+
if (column.table) out.table = column.table;
|
|
307
|
+
if (column.onDelete) out.onDelete = column.onDelete;
|
|
308
|
+
if (column.min !== null) out.min = column.min;
|
|
309
|
+
if (column.default !== undefined) out.default = column.default;
|
|
310
|
+
return out;
|
|
311
|
+
})
|
|
312
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -344,6 +344,8 @@ import { citesUrl, citedPages, selectCitedSources } from './citations';
|
|
|
344
344
|
import {
|
|
345
345
|
refreshOAuthToken,
|
|
346
346
|
OAuthReauthRequiredError,
|
|
347
|
+
OAuthRefreshError,
|
|
348
|
+
refreshFailureReauthReason,
|
|
347
349
|
buildReauthMetadata,
|
|
348
350
|
clearReauthMetadata,
|
|
349
351
|
isCredentialNeedingReauth
|
|
@@ -557,6 +559,8 @@ export const utils = {
|
|
|
557
559
|
selectCitedSources,
|
|
558
560
|
refreshOAuthToken,
|
|
559
561
|
OAuthReauthRequiredError,
|
|
562
|
+
OAuthRefreshError,
|
|
563
|
+
refreshFailureReauthReason,
|
|
560
564
|
buildReauthMetadata,
|
|
561
565
|
clearReauthMetadata,
|
|
562
566
|
isCredentialNeedingReauth,
|
package/src/oauth.ts
CHANGED
|
@@ -24,6 +24,21 @@ export class OAuthReauthRequiredError extends Error {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// The token endpoint answered with an error that isn't a reauth code. `status`
|
|
28
|
+
// tells a rejection (4xx) from an outage (5xx); a network failure never gets
|
|
29
|
+
// here, it throws fetch's own error.
|
|
30
|
+
export class OAuthRefreshError extends Error {
|
|
31
|
+
readonly status: number;
|
|
32
|
+
readonly code: string | undefined;
|
|
33
|
+
|
|
34
|
+
constructor(status: number, code: string | undefined, body: string) {
|
|
35
|
+
super(`oauth refresh failed (${status}): ${body}`);
|
|
36
|
+
this.name = 'OAuthRefreshError';
|
|
37
|
+
this.status = status;
|
|
38
|
+
this.code = code;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
const parseErrorCode = (body: string): string | undefined => {
|
|
28
43
|
try {
|
|
29
44
|
return (JSON.parse(body) as { error?: string })?.error;
|
|
@@ -52,7 +67,7 @@ export const refreshOAuthToken = async (
|
|
|
52
67
|
if (code && constants.REAUTH_ERROR_CODES.includes(code)) {
|
|
53
68
|
throw new OAuthReauthRequiredError(code);
|
|
54
69
|
}
|
|
55
|
-
throw new
|
|
70
|
+
throw new OAuthRefreshError(response.status, code, body);
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
const tokens = (await response.json()) as {
|
|
@@ -95,6 +110,32 @@ export const clearReauthMetadata = (
|
|
|
95
110
|
return Object.keys(next).length > 0 ? next : null;
|
|
96
111
|
};
|
|
97
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Why a failed refresh should flag the credential for a reconnect, or null
|
|
115
|
+
* when it shouldn't. A reauth code flags it at once. Any other rejection
|
|
116
|
+
* (4xx) flags it only once the access token has been expired for
|
|
117
|
+
* CREDENTIAL_REFRESH_GIVE_UP_MS, so a one-off doesn't. Never flagged: an
|
|
118
|
+
* outage (5xx), a network error, rate limiting (429), and errors about our
|
|
119
|
+
* own client id / secret, which no reconnect can fix.
|
|
120
|
+
*/
|
|
121
|
+
export const refreshFailureReauthReason = (
|
|
122
|
+
err: unknown,
|
|
123
|
+
expiresAt: Date | null,
|
|
124
|
+
now = Date.now()
|
|
125
|
+
): string | null => {
|
|
126
|
+
if (err instanceof OAuthReauthRequiredError) return err.code;
|
|
127
|
+
if (!(err instanceof OAuthRefreshError)) return null;
|
|
128
|
+
if (err.status < 400 || err.status >= 500 || err.status === 429) return null;
|
|
129
|
+
if (err.code && constants.OAUTH_CLIENT_ERROR_CODES.includes(err.code)) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
if (!expiresAt) return null;
|
|
133
|
+
if (now - expiresAt.getTime() < constants.CREDENTIAL_REFRESH_GIVE_UP_MS) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return err.code ? `refresh_rejected:${err.code}` : 'refresh_rejected';
|
|
137
|
+
};
|
|
138
|
+
|
|
98
139
|
export const isCredentialNeedingReauth = (metadata: unknown): boolean => {
|
|
99
140
|
if (!metadata || typeof metadata !== 'object') return false;
|
|
100
141
|
return (metadata as Record<string, unknown>).needsReauth === true;
|
package/src/resourceRerank.ts
CHANGED
|
@@ -15,7 +15,8 @@ type RerankResponse = {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* The cross-encoder second stage for resource search, or undefined when
|
|
18
|
+
* The cross-encoder second stage for resource search, or undefined when it is
|
|
19
|
+
* off (RESOURCE_SEARCH_RERANK_ENABLED, unless `enabled` overrides it) or the
|
|
19
20
|
* Worker has no `AI` binding — local dev without Workers AI keeps plain fusion.
|
|
20
21
|
*
|
|
21
22
|
* Returns one score per document, in input order. A response that doesn't
|
|
@@ -23,10 +24,12 @@ type RerankResponse = {
|
|
|
23
24
|
* ranking would silently drop the chunks it left out.
|
|
24
25
|
*/
|
|
25
26
|
export const createResourceReranker = (
|
|
26
|
-
source: EnvSource
|
|
27
|
+
source: EnvSource,
|
|
28
|
+
enabled: boolean = constants.RESOURCE_SEARCH_RERANK_ENABLED
|
|
27
29
|
):
|
|
28
30
|
| ((query: string, documents: string[]) => Promise<number[] | null>)
|
|
29
31
|
| undefined => {
|
|
32
|
+
if (!enabled) return undefined;
|
|
30
33
|
const ai = (source.env as { AI?: RerankAiBinding } | undefined)?.AI;
|
|
31
34
|
if (!ai) return undefined;
|
|
32
35
|
|