@ganju/utils 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/constants.d.ts +26 -0
  2. package/dist/constants.d.ts.map +1 -1
  3. package/dist/constants.js +107 -9
  4. package/dist/customCodeToken.d.ts +11 -0
  5. package/dist/customCodeToken.d.ts.map +1 -1
  6. package/dist/customCodeToken.js +41 -1
  7. package/dist/data.d.ts +13 -0
  8. package/dist/data.d.ts.map +1 -0
  9. package/dist/data.js +103 -0
  10. package/dist/dataApi.d.ts +578 -0
  11. package/dist/dataApi.d.ts.map +1 -0
  12. package/dist/dataApi.js +399 -0
  13. package/dist/dataCompiler.d.ts +68 -0
  14. package/dist/dataCompiler.d.ts.map +1 -0
  15. package/dist/dataCompiler.js +730 -0
  16. package/dist/dataCsv.d.ts +20 -0
  17. package/dist/dataCsv.d.ts.map +1 -0
  18. package/dist/dataCsv.js +102 -0
  19. package/dist/dataDdl.d.ts +29 -0
  20. package/dist/dataDdl.d.ts.map +1 -0
  21. package/dist/dataDdl.js +354 -0
  22. package/dist/dataEngine.d.ts +143 -0
  23. package/dist/dataEngine.d.ts.map +1 -0
  24. package/dist/dataEngine.js +2042 -0
  25. package/dist/dataIdentifiers.d.ts +8 -0
  26. package/dist/dataIdentifiers.d.ts.map +1 -0
  27. package/dist/dataIdentifiers.js +55 -0
  28. package/dist/dataImport.d.ts +79 -0
  29. package/dist/dataImport.d.ts.map +1 -0
  30. package/dist/dataImport.js +453 -0
  31. package/dist/dataQueries.d.ts +73 -0
  32. package/dist/dataQueries.d.ts.map +1 -0
  33. package/dist/dataQueries.js +513 -0
  34. package/dist/dataRpc.d.ts +150 -0
  35. package/dist/dataRpc.d.ts.map +1 -0
  36. package/dist/dataRpc.js +2 -0
  37. package/dist/dataTools.d.ts +40 -0
  38. package/dist/dataTools.d.ts.map +1 -0
  39. package/dist/dataTools.js +280 -0
  40. package/dist/dataTypes.d.ts +159 -0
  41. package/dist/dataTypes.d.ts.map +1 -0
  42. package/dist/dataTypes.js +160 -0
  43. package/dist/index.d.ts +394 -8
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +4 -0
  46. package/dist/oauth.d.ts +14 -0
  47. package/dist/oauth.d.ts.map +1 -1
  48. package/dist/oauth.js +42 -2
  49. package/dist/resourceRerank.d.ts +3 -2
  50. package/dist/resourceRerank.d.ts.map +1 -1
  51. package/dist/resourceRerank.js +5 -2
  52. package/dist/schema.d.ts +376 -4
  53. package/dist/schema.d.ts.map +1 -1
  54. package/dist/schema.js +268 -1
  55. package/dist/sdkConstants.d.ts +1 -0
  56. package/dist/sdkConstants.d.ts.map +1 -1
  57. package/dist/sdkConstants.js +2 -1
  58. package/package.json +17 -2
  59. package/src/constants.ts +121 -9
  60. package/src/customCodeToken.ts +74 -0
  61. package/src/data.ts +108 -0
  62. package/src/dataApi.ts +479 -0
  63. package/src/dataCompiler.ts +952 -0
  64. package/src/dataCsv.ts +99 -0
  65. package/src/dataDdl.ts +419 -0
  66. package/src/dataEngine.ts +2773 -0
  67. package/src/dataIdentifiers.ts +63 -0
  68. package/src/dataImport.ts +557 -0
  69. package/src/dataQueries.ts +730 -0
  70. package/src/dataRpc.ts +132 -0
  71. package/src/dataTools.ts +338 -0
  72. package/src/dataTypes.ts +318 -0
  73. package/src/index.ts +13 -1
  74. package/src/oauth.ts +42 -1
  75. package/src/resourceRerank.ts +5 -2
  76. package/src/schema.ts +315 -1
  77. package/src/sdkConstants.ts +1 -0
@@ -0,0 +1,318 @@
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
+ // The zone the owner set for the database: datetimes come back in it for
108
+ // callers that send none. Null when unset (UTC).
109
+ timeZone?: string | null;
110
+ }
111
+
112
+ // Who is calling and under which rules. `actor` is recorded on every row a
113
+ // call writes; the storage layer refuses a write without one.
114
+ export interface DataCaller {
115
+ // user:<id> · cli:<tokenId> · mcp:<userId> · channel:<participantId> ·
116
+ // function:<toolName> · trigger:<id> · system:<job>
117
+ actor: string;
118
+ // Apply each table's assistant access. Set for channel bots only.
119
+ restricted?: boolean;
120
+ // Customer-scoped tables: fill and filter `participant_id` with this.
121
+ participantId?: string;
122
+ // Only these tables exist for the caller (a function's `data.tables`):
123
+ // every other one is hidden, through relations and filters too.
124
+ tables?: string[];
125
+ // Datetimes come back in this zone, with the offset.
126
+ timeZone?: string;
127
+ limits?: DataCallerLimits;
128
+ }
129
+
130
+ export interface DataCallerLimits {
131
+ // Writes that add data stop at this size (the plan's ceiling).
132
+ maxBytes?: number | null;
133
+ maxTables?: number | null;
134
+ // The organization left the paid plan: reads and exports only.
135
+ readOnly?: boolean;
136
+ }
137
+
138
+ export interface DataUsage {
139
+ rowsRead: number;
140
+ rowsWritten: number;
141
+ }
142
+
143
+ export const DATA_ERROR_CODES = [
144
+ 'invalid',
145
+ 'not_found',
146
+ 'forbidden',
147
+ 'conflict',
148
+ 'constraint',
149
+ 'guard',
150
+ 'too_large',
151
+ 'full',
152
+ 'read_only',
153
+ 'deleted',
154
+ 'not_initialised',
155
+ 'restore',
156
+ 'internal'
157
+ ] as const;
158
+ export type DataErrorCode = (typeof DATA_ERROR_CODES)[number];
159
+
160
+ // The one error the database object returns. Its message is written for the
161
+ // person or model that made the call, never raw SQLite text.
162
+ export class DataError extends Error {
163
+ readonly code: DataErrorCode;
164
+ readonly details: Record<string, unknown> | undefined;
165
+
166
+ constructor(
167
+ message: string,
168
+ code: DataErrorCode = 'invalid',
169
+ details?: Record<string, unknown>
170
+ ) {
171
+ super(message);
172
+ this.name = 'DataError';
173
+ this.code = code;
174
+ this.details = details;
175
+ }
176
+
177
+ // What the API answers with; its error handler reads both.
178
+ get status(): 400 | 403 | 404 | 409 | 500 {
179
+ return DATA_ERROR_STATUS[this.code];
180
+ }
181
+
182
+ toBody(): Record<string, unknown> {
183
+ return { code: this.code, ...(this.details ? { details: this.details } : {}) };
184
+ }
185
+ }
186
+
187
+ const DATA_ERROR_STATUS: Record<DataErrorCode, 400 | 403 | 404 | 409 | 500> = {
188
+ invalid: 400,
189
+ not_found: 404,
190
+ forbidden: 403,
191
+ conflict: 409,
192
+ constraint: 409,
193
+ guard: 409,
194
+ too_large: 400,
195
+ full: 409,
196
+ read_only: 403,
197
+ deleted: 409,
198
+ not_initialised: 404,
199
+ restore: 400,
200
+ internal: 500
201
+ };
202
+
203
+ export const isDataError = (error: unknown): error is DataError =>
204
+ error instanceof DataError;
205
+
206
+ // What every database object method returns. Errors travel as values, not
207
+ // throws: an RPC exception keeps only its message, and the caller needs the
208
+ // code and the rows read before it.
209
+ export type DataResult<T> =
210
+ | { ok: true; value: T; usage: DataUsage }
211
+ | {
212
+ ok: false;
213
+ error: {
214
+ code: DataErrorCode;
215
+ message: string;
216
+ details?: Record<string, unknown>;
217
+ };
218
+ usage: DataUsage;
219
+ };
220
+
221
+ // Limits. SQLite in a Durable Object allows 100 columns per table, 100 bound
222
+ // parameters per statement and 100 KB per statement; the rest are ours.
223
+ export const DATA_MAX_PARAMS = 100;
224
+ export const DATA_MAX_STATEMENT_BYTES = 90_000;
225
+ // 100 minus id, created_at, updated_at, version, created_by, updated_by and
226
+ // participant_id.
227
+ export const DATA_MAX_COLUMNS = 93;
228
+ export const DATA_MAX_TABLES = 50;
229
+ export const DATA_DEFAULT_PAGE = 20;
230
+ export const DATA_MAX_PAGE = 500;
231
+ export const DATA_MAX_RESULT_BYTES = 1024 * 1024;
232
+ // Past this many rows, an aggregate or a sort on an unindexed column needs a
233
+ // filter on an indexed one: the object serves one request at a time.
234
+ export const DATA_LARGE_TABLE_ROWS = 200_000;
235
+ export const DATA_MAX_WRITE_OPERATIONS = 50;
236
+ export const DATA_MAX_INSERT_ROWS = 500;
237
+ export const DATA_MAX_ROWS_PER_WRITE = 5_000;
238
+ export const DATA_MAX_VALUE_BYTES = 1_000_000;
239
+ export const DATA_MAX_DESCRIPTION = 300;
240
+ export const DATA_MAX_LABEL = 100;
241
+ export const DATA_MAX_ENUM_VALUES = 100;
242
+ export const DATA_MAX_IN_VALUES = 50;
243
+ export const DATA_MAX_SORT_KEYS = 3;
244
+ export const DATA_MAX_AGGREGATES = 5;
245
+ export const DATA_MAX_GROUP_BY = 3;
246
+ export const DATA_MAX_GROUPS = 100;
247
+ // A type change copies the table in one request up to this size. A larger
248
+ // table is copied in the background, this many rows a step (tens of
249
+ // milliseconds, so other requests are served in between), and writes that
250
+ // would change its rows wait until it is done.
251
+ export const DATA_REBUILD_MAX_ROWS = 100_000;
252
+ export const DATA_COPY_STEP_ROWS = 5_000;
253
+ // A delete preview follows cascades through up to this many rows; past it,
254
+ // its counts are lower bounds.
255
+ export const DATA_IMPACT_MAX_ROWS = 200_000;
256
+ export const DATA_IMPACT_MAX_IDS = DATA_MAX_ROWS_PER_WRITE;
257
+ export const DATA_HISTORY_DAYS = 90;
258
+
259
+ // What deleting rows does elsewhere: one entry per relation column that points
260
+ // at a row the delete removes, cascades followed all the way down. `depth` is
261
+ // how many cascades away from the deleted rows the pointed-at table is.
262
+ export interface DataDeleteImpact {
263
+ table: string;
264
+ rows: number;
265
+ effects: {
266
+ table: string;
267
+ column: string;
268
+ onDelete: DataOnDelete;
269
+ rows: number;
270
+ depth: number;
271
+ }[];
272
+ // A restrict would refuse the delete.
273
+ blocked: boolean;
274
+ // Past DATA_IMPACT_MAX_ROWS: every count is at least what it says.
275
+ truncated: boolean;
276
+ }
277
+
278
+ // A table copy running in the background, as callers see it.
279
+ export interface DataTableCopy {
280
+ table: string;
281
+ // The column whose change or drop started it.
282
+ column: string;
283
+ kind: 'change' | 'drop';
284
+ rows: number;
285
+ copied: number;
286
+ startedAt: string;
287
+ actor: string;
288
+ status: 'running' | 'failed';
289
+ error: string | null;
290
+ }
291
+
292
+ // The table as the owner would write it again: what `ganju.data.json` and an
293
+ // export's schema.json hold.
294
+ export const toTableInput = (table: DataTable): DataTableInput => ({
295
+ name: table.name,
296
+ label: table.label,
297
+ description: table.description,
298
+ displayColumn: table.displayColumn,
299
+ access: table.access,
300
+ customerScoped: table.customerScoped,
301
+ history: table.history,
302
+ columns: table.columns.map(column => {
303
+ const out: DataColumnInput = { name: column.name, type: column.type };
304
+ if (column.label) out.label = column.label;
305
+ if (column.description) out.description = column.description;
306
+ if (column.required) out.required = true;
307
+ if (column.unique) out.unique = true;
308
+ if (column.filterable) out.filterable = true;
309
+ if (column.searchable) out.searchable = true;
310
+ if (column.personal) out.personal = true;
311
+ if (column.values) out.values = column.values;
312
+ if (column.table) out.table = column.table;
313
+ if (column.onDelete) out.onDelete = column.onDelete;
314
+ if (column.min !== null) out.min = column.min;
315
+ if (column.default !== undefined) out.default = column.default;
316
+ return out;
317
+ })
318
+ });
package/src/index.ts CHANGED
@@ -109,12 +109,17 @@ import {
109
109
  import {
110
110
  mintCustomCodeToken,
111
111
  verifyCustomCodeToken,
112
+ mintCustomCodeCallToken,
113
+ verifyCustomCodeCallToken,
112
114
  customCodeScriptName,
113
115
  customCodePreviewScriptName,
114
116
  customCodeUploadName,
115
117
  customCodePreviewUploadName
116
118
  } from './customCodeToken';
117
- import type { CustomCodeTokenPayload } from './customCodeToken';
119
+ import type {
120
+ CustomCodeCallPayload,
121
+ CustomCodeTokenPayload
122
+ } from './customCodeToken';
118
123
  import {
119
124
  mintContainmentToken,
120
125
  verifyContainmentToken
@@ -344,6 +349,8 @@ import { citesUrl, citedPages, selectCitedSources } from './citations';
344
349
  import {
345
350
  refreshOAuthToken,
346
351
  OAuthReauthRequiredError,
352
+ OAuthRefreshError,
353
+ refreshFailureReauthReason,
347
354
  buildReauthMetadata,
348
355
  clearReauthMetadata,
349
356
  isCredentialNeedingReauth
@@ -409,6 +416,8 @@ export const utils = {
409
416
  listResourcesPage,
410
417
  mintCustomCodeToken,
411
418
  verifyCustomCodeToken,
419
+ mintCustomCodeCallToken,
420
+ verifyCustomCodeCallToken,
412
421
  customCodeScriptName,
413
422
  customCodePreviewScriptName,
414
423
  customCodeUploadName,
@@ -557,6 +566,8 @@ export const utils = {
557
566
  selectCitedSources,
558
567
  refreshOAuthToken,
559
568
  OAuthReauthRequiredError,
569
+ OAuthRefreshError,
570
+ refreshFailureReauthReason,
560
571
  buildReauthMetadata,
561
572
  clearReauthMetadata,
562
573
  isCredentialNeedingReauth,
@@ -653,6 +664,7 @@ export type {
653
664
  CustomCodeManifest,
654
665
  CustomCodeSendFile,
655
666
  CustomCodeCreateResource,
667
+ CustomCodeCallPayload,
656
668
  CustomCodeTokenPayload,
657
669
  ContainmentTokenPayload,
658
670
  OAuthProviderConfig,
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 Error(`oauth refresh failed (${response.status}): ${body}`);
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;
@@ -15,7 +15,8 @@ type RerankResponse = {
15
15
  };
16
16
 
17
17
  /**
18
- * The cross-encoder second stage for resource search, or undefined when the
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