zitejs 0.9.95 → 0.9.96
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/cjs/backend/index.d.ts +18 -4
- package/dist/cjs/backend/index.js +17 -3
- package/dist/cjs/bundle/index.js +7 -2
- package/dist/cjs/runtime/index.d.ts +23 -6
- package/dist/cjs/sync/lib.js +149 -54
- package/dist/esm/backend/index.d.ts +18 -4
- package/dist/esm/backend/index.js +17 -3
- package/dist/esm/bundle/index.js +7 -2
- package/dist/esm/runtime/index.d.ts +23 -6
- package/dist/esm/sync/lib.js +149 -54
- package/package.json +2 -2
|
@@ -27,13 +27,22 @@ export type ZiteErrorCode = "BAD_REQUEST" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_
|
|
|
27
27
|
* through a fixed table (BAD_REQUEST -> 400, and so on); it reads `code` and
|
|
28
28
|
* nothing else, so an error carrying only a numeric `statusCode` fell through
|
|
29
29
|
* to a generic 500.
|
|
30
|
+
*
|
|
31
|
+
* Both call shapes, mirroring the worker's own class (`zite-runtime.ts`): the
|
|
32
|
+
* object form is preferred, and the positional form is what pre-monorepo app
|
|
33
|
+
* code was written against. Typing only the object form would have failed the
|
|
34
|
+
* typecheck of every migrated app that throws one.
|
|
30
35
|
*/
|
|
31
36
|
export declare class ZiteError extends Error {
|
|
32
37
|
code: ZiteErrorCode;
|
|
38
|
+
/** Short, non-technical message suitable for showing to an end user. */
|
|
39
|
+
userFacingMessage?: string;
|
|
33
40
|
constructor(options: {
|
|
34
41
|
code: ZiteErrorCode;
|
|
35
42
|
message: string;
|
|
43
|
+
userFacingMessage?: string;
|
|
36
44
|
});
|
|
45
|
+
constructor(message: string, code?: ZiteErrorCode);
|
|
37
46
|
}
|
|
38
47
|
/**
|
|
39
48
|
* Structurally matches a zod schema. `TIn` is the schema's *input* type, which
|
|
@@ -49,7 +58,7 @@ export type ZiteStreamInterface = {
|
|
|
49
58
|
write: (data: unknown) => Promise<void>;
|
|
50
59
|
forward: (asyncIterable: AsyncIterable<string>) => Promise<string>;
|
|
51
60
|
};
|
|
52
|
-
export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TRawInput = TInput> {
|
|
61
|
+
export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput> {
|
|
53
62
|
description?: string;
|
|
54
63
|
inputSchema?: SchemaLike<TInput, TRawInput>;
|
|
55
64
|
outputSchema?: SchemaLike<TOutput>;
|
|
@@ -61,12 +70,17 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
|
|
|
61
70
|
* Declaring one widens `context` so `context.user` must be null-checked.
|
|
62
71
|
*/
|
|
63
72
|
schedule?: TSchedule;
|
|
64
|
-
|
|
73
|
+
/**
|
|
74
|
+
* When set, the endpoint can also be triggered by an inbound webhook. Like
|
|
75
|
+
* `schedule`, this widens `context` — the workflow-runner sends
|
|
76
|
+
* `{ user: null }` for a webhook fire exactly as it does for a cron one.
|
|
77
|
+
*/
|
|
78
|
+
webhook?: TWebhook;
|
|
65
79
|
execute: (params: {
|
|
66
80
|
input: TInput;
|
|
67
|
-
context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;
|
|
81
|
+
context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;
|
|
68
82
|
} & (TStream extends true ? {
|
|
69
83
|
stream: ZiteStreamInterface;
|
|
70
84
|
} : {})) => Promise<TOutput> | TOutput;
|
|
71
85
|
}
|
|
72
|
-
export declare function createEndpoint<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TRawInput = TInput>(config: EndpointConfig<TInput, TOutput, TStream, TSchedule, TRawInput>): EndpointConfig<TInput, TOutput, TStream, TSchedule, TRawInput>;
|
|
86
|
+
export declare function createEndpoint<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput>(config: EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput>): EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput>;
|
|
@@ -7,13 +7,27 @@ exports.createEndpoint = createEndpoint;
|
|
|
7
7
|
* through a fixed table (BAD_REQUEST -> 400, and so on); it reads `code` and
|
|
8
8
|
* nothing else, so an error carrying only a numeric `statusCode` fell through
|
|
9
9
|
* to a generic 500.
|
|
10
|
+
*
|
|
11
|
+
* Both call shapes, mirroring the worker's own class (`zite-runtime.ts`): the
|
|
12
|
+
* object form is preferred, and the positional form is what pre-monorepo app
|
|
13
|
+
* code was written against. Typing only the object form would have failed the
|
|
14
|
+
* typecheck of every migrated app that throws one.
|
|
10
15
|
*/
|
|
11
16
|
class ZiteError extends Error {
|
|
12
17
|
code;
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
/** Short, non-technical message suitable for showing to an end user. */
|
|
19
|
+
userFacingMessage;
|
|
20
|
+
constructor(optionsOrMessage, legacyCode) {
|
|
21
|
+
if (typeof optionsOrMessage === "string") {
|
|
22
|
+
super(optionsOrMessage);
|
|
23
|
+
this.code = legacyCode ?? "INTERNAL_ERROR";
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
super(optionsOrMessage.message);
|
|
27
|
+
this.code = optionsOrMessage.code;
|
|
28
|
+
this.userFacingMessage = optionsOrMessage.userFacingMessage;
|
|
29
|
+
}
|
|
15
30
|
this.name = "ZiteError";
|
|
16
|
-
this.code = options.code;
|
|
17
31
|
}
|
|
18
32
|
}
|
|
19
33
|
exports.ZiteError = ZiteError;
|
package/dist/cjs/bundle/index.js
CHANGED
|
@@ -599,8 +599,13 @@ async function runBundle() {
|
|
|
599
599
|
if (appFlag !== -1 && args[appFlag + 1]) {
|
|
600
600
|
baseDir = path.resolve(baseDir, 'apps', args[appFlag + 1]);
|
|
601
601
|
}
|
|
602
|
-
// If explicit endpoint names passed, use those; otherwise find all
|
|
603
|
-
|
|
602
|
+
// If explicit endpoint names passed, use those; otherwise find all.
|
|
603
|
+
// Skipping `--app`'s VALUE as well as the flag: it is a positional arg, so
|
|
604
|
+
// filtering on the `--` prefix alone treated the app directory name as an
|
|
605
|
+
// endpoint name — `zitejs bundle --app myapp` tried to bundle
|
|
606
|
+
// `src/api/myapp.ts`, found nothing else, and reported every real endpoint
|
|
607
|
+
// as missing.
|
|
608
|
+
const explicitNames = args.filter((a, i) => !a.startsWith('--') && !(appFlag !== -1 && i === appFlag + 1));
|
|
604
609
|
let endpointNames;
|
|
605
610
|
if (explicitNames.length > 0) {
|
|
606
611
|
endpointNames = explicitNames;
|
|
@@ -2,21 +2,30 @@ import type { NotificationsCreateParams, NotificationsCreateResult } from "../no
|
|
|
2
2
|
/**
|
|
3
3
|
* A comparison against one field. Mirrors base-runner's operator set
|
|
4
4
|
* (`LegacyWhereConditionOperators`); anything else in the object is ignored.
|
|
5
|
+
*
|
|
6
|
+
* The operand types deliberately match the pre-monorepo `FilterOperators`
|
|
7
|
+
* rather than tightening to the field's own type. The value of typing filters
|
|
8
|
+
* is in the KEY — an unknown key silently returns every row — and tightening
|
|
9
|
+
* the operands buys little while breaking real migrated code: dates are ISO
|
|
10
|
+
* strings here but 1.0 accepted `number | Date` on every comparison, and
|
|
11
|
+
* `contains` was `any`.
|
|
5
12
|
*/
|
|
6
13
|
export type FilterCondition<V> = {
|
|
7
14
|
/** Substring match (text fields) or "has any of these" (linked records). */
|
|
8
|
-
contains?:
|
|
15
|
+
contains?: any;
|
|
9
16
|
/** Not equal, or — against `null` — "is set". */
|
|
10
17
|
not?: V | null;
|
|
11
18
|
/** Empty array means "no filter", not "match nothing". */
|
|
12
19
|
in?: V extends Array<infer E> ? E[] : V[];
|
|
13
20
|
/** Empty array means "no filter", not "match everything". */
|
|
14
21
|
notIn?: V extends Array<infer E> ? E[] : V[];
|
|
15
|
-
lt?: V;
|
|
16
|
-
lte?: V;
|
|
17
|
-
gt?: V;
|
|
18
|
-
gte?: V;
|
|
22
|
+
lt?: V | number | Date;
|
|
23
|
+
lte?: V | number | Date;
|
|
24
|
+
gt?: V | number | Date;
|
|
25
|
+
gte?: V | number | Date;
|
|
19
26
|
};
|
|
27
|
+
/** Pre-monorepo name for {@link FilterCondition}. Kept so migrated app code that spells the type out still compiles. */
|
|
28
|
+
export type FilterOperators<V> = FilterCondition<V> | V;
|
|
20
29
|
/**
|
|
21
30
|
* Filters keyed by the record's own field names. Typed rather than `unknown`
|
|
22
31
|
* because an unrecognized key is not an error at runtime — it passes through
|
|
@@ -24,8 +33,16 @@ export type FilterCondition<V> = {
|
|
|
24
33
|
* comes back **unfiltered**. A typo silently returns every row.
|
|
25
34
|
*/
|
|
26
35
|
export type RecordFilters<T> = {
|
|
27
|
-
[K in keyof T]?: T[K] | FilterCondition<T[K]>;
|
|
36
|
+
[K in keyof T]?: T[K] | (T[K] extends Array<infer E> ? E : never) | FilterCondition<T[K]>;
|
|
28
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* Resolves a zod schema to the type it produces. `createEndpoint` infers this
|
|
40
|
+
* for you, so it is rarely needed — it exists because the pre-monorepo SDK
|
|
41
|
+
* exposed it and migrated app code may still name it.
|
|
42
|
+
*/
|
|
43
|
+
export type InferSchemaType<T> = T extends {
|
|
44
|
+
_output: infer U;
|
|
45
|
+
} ? U : T;
|
|
29
46
|
export interface TableFindAllOptions<T = Record<string, unknown>> {
|
|
30
47
|
limit?: number;
|
|
31
48
|
offset?: number;
|
package/dist/cjs/sync/lib.js
CHANGED
|
@@ -52,10 +52,15 @@ const FIELD_INPUT_TYPE_MAP = {
|
|
|
52
52
|
user: "string | string[] | null",
|
|
53
53
|
};
|
|
54
54
|
/**
|
|
55
|
-
* Computed and system-managed fields
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
55
|
+
* Computed and system-managed fields, omitted from the generated input type
|
|
56
|
+
* rather than typed and then refused at runtime.
|
|
57
|
+
*
|
|
58
|
+
* Mirrors `COMPUTED_FIELD_TYPES` in the product's own `shared/bases/fields`.
|
|
59
|
+
* The first six are rejected by `createRecordDataSchema` — each has a
|
|
60
|
+
* `refine(val => val == null)`, so a non-null write errors (null is a no-op
|
|
61
|
+
* clear). `created_at` / `updated_at` are refused differently: they have no
|
|
62
|
+
* column at all, so a write is silently dropped, which is the worse outcome to
|
|
63
|
+
* leave typed as writable.
|
|
59
64
|
*/
|
|
60
65
|
const READ_ONLY_FIELD_TYPES = new Set([
|
|
61
66
|
"lookup",
|
|
@@ -64,6 +69,8 @@ const READ_ONLY_FIELD_TYPES = new Set([
|
|
|
64
69
|
"formula",
|
|
65
70
|
"autonumber",
|
|
66
71
|
"updated_by",
|
|
72
|
+
"created_at",
|
|
73
|
+
"updated_at",
|
|
67
74
|
]);
|
|
68
75
|
/**
|
|
69
76
|
* Emitted once per `db.ts`. The old inline `{ url: string; name?: string }` was
|
|
@@ -110,6 +117,19 @@ const DURATION_FORMAT_EXAMPLES = {
|
|
|
110
117
|
"h:mm:ss.ss": "1:23:03.00",
|
|
111
118
|
"h:mm:ss.sss": "1:23:03.000",
|
|
112
119
|
};
|
|
120
|
+
// Naming the format alone ("display as `european` format") doesn't say what to
|
|
121
|
+
// render — these mirror the pickers in the database UI, which is what the user
|
|
122
|
+
// chose from. `local` is the viewer's system locale, so it has no fixed example.
|
|
123
|
+
const DATE_FORMAT_EXAMPLES = {
|
|
124
|
+
long: "January 15, 2024",
|
|
125
|
+
us: "1/15/2024",
|
|
126
|
+
european: "15/01/2024",
|
|
127
|
+
iso: "2024-01-15",
|
|
128
|
+
};
|
|
129
|
+
const withDateFormatExample = (format) => {
|
|
130
|
+
const example = DATE_FORMAT_EXAMPLES[format];
|
|
131
|
+
return example ? `"${format}" format (e.g. ${example})` : `"${format}" format`;
|
|
132
|
+
};
|
|
113
133
|
function toPascalCase(name) {
|
|
114
134
|
const pascal = name
|
|
115
135
|
.replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase())
|
|
@@ -188,13 +208,13 @@ function fieldJsdoc(schemaField, table, schema) {
|
|
|
188
208
|
if (def.type === "date") {
|
|
189
209
|
const tpl = def.template;
|
|
190
210
|
if (tpl.dateFormat)
|
|
191
|
-
parts.push(`Date-only field (YYYY-MM-DD string), null when unset, display as
|
|
211
|
+
parts.push(`Date-only field (YYYY-MM-DD string), null when unset, display as ${withDateFormatExample(tpl.dateFormat)}`);
|
|
192
212
|
}
|
|
193
213
|
if (def.type === "datetime") {
|
|
194
214
|
const tpl = def.template;
|
|
195
215
|
const timeParts = ["Date+time field (ISO 8601 timestamp), null when unset"];
|
|
196
216
|
if (tpl.dateFormat)
|
|
197
|
-
timeParts.push(`date: ${tpl.dateFormat}`);
|
|
217
|
+
timeParts.push(`date: ${withDateFormatExample(tpl.dateFormat)}`);
|
|
198
218
|
if (tpl.timeFormat)
|
|
199
219
|
timeParts.push(`time: ${tpl.timeFormat}`);
|
|
200
220
|
if (tpl.timezone)
|
|
@@ -393,13 +413,13 @@ function generateDbTs(schema) {
|
|
|
393
413
|
lines.push("// operators: contains, not, in, notIn, lt, lte, gt, gte");
|
|
394
414
|
lines.push('// `not: null` means "is set"; `in`/`notIn` with an empty array apply NO filter');
|
|
395
415
|
lines.push("// sort: [{ field: 'fieldName', direction: 'asc' | 'desc' }]");
|
|
396
|
-
lines.push("// limit defaults to 500, max
|
|
416
|
+
lines.push("// limit defaults to 500, max 2000; offset is a row count (a number)");
|
|
397
417
|
lines.push("// .findOne({ id?, filters?, fields? }) → T | undefined");
|
|
398
418
|
lines.push("// .create({ record }) → T");
|
|
399
419
|
lines.push("// .update({ id, record }) → { id: string, fields: Partial<T> }");
|
|
400
420
|
lines.push("// .delete({ id }) → { success: true, id: string }");
|
|
401
421
|
lines.push("// .bulkCreate({ records, matchOn? }) → { success: boolean, records: T[] }");
|
|
402
|
-
lines.push("// up to
|
|
422
|
+
lines.push("// up to 100 records per call; matchOn upserts on those fields");
|
|
403
423
|
lines.push("//");
|
|
404
424
|
lines.push("// Reading these types:");
|
|
405
425
|
lines.push("// SDK names are stable identifiers — they do NOT change when a table or field is");
|
|
@@ -410,8 +430,9 @@ function generateDbTs(schema) {
|
|
|
410
430
|
lines.push("// display yourself, following the display config in each field's comment.");
|
|
411
431
|
lines.push("// PERCENT fields are stored as decimals — 0.5 is 50%, 1.0 is 100%.");
|
|
412
432
|
lines.push("// LINKED RECORD fields hold UUID record ids from findOne/findAll — never invent one.");
|
|
413
|
-
lines.push("// Computed fields (formula, rollup, lookup, autonumber
|
|
414
|
-
lines.push("//
|
|
433
|
+
lines.push("// Computed and system-managed fields (formula, rollup, lookup, autonumber,");
|
|
434
|
+
lines.push("// created/updated timestamps and more) are absent from the *Input types —");
|
|
435
|
+
lines.push("// a write to one is rejected or silently dropped.");
|
|
415
436
|
lines.push("//");
|
|
416
437
|
lines.push("// Zite auth users:");
|
|
417
438
|
lines.push("// zite.auth.findAllUsers({ appIds?, filter?, filters?, sort?, limit?, offset? })");
|
|
@@ -425,7 +446,7 @@ function generateDbTs(schema) {
|
|
|
425
446
|
lines.push("// → { rows: Record<string, unknown>[], columns, rowCount, truncated }");
|
|
426
447
|
lines.push("// Capped at 2000 rows — `truncated: true` means there were more, so paginate in");
|
|
427
448
|
lines.push("// SQL rather than assuming you got everything. Queries time out after 10s.");
|
|
428
|
-
lines.push("// columns[].name is the SDK name; columns[].originalName is the
|
|
449
|
+
lines.push("// columns[].name is the SDK name; columns[].originalName is the underlying", "// database column (or your own SQL alias).");
|
|
429
450
|
lines.push("//");
|
|
430
451
|
lines.push("// SQL guidelines:");
|
|
431
452
|
lines.push("// - Use SDK names for tables (PascalCase) and fields (camelCase)");
|
|
@@ -483,38 +504,74 @@ function generateDbTs(schema) {
|
|
|
483
504
|
lines.push("");
|
|
484
505
|
return lines.join("\n");
|
|
485
506
|
}
|
|
507
|
+
const NOT_AN_ENDPOINT = {
|
|
508
|
+
isEndpoint: false,
|
|
509
|
+
hasDefaultExport: false,
|
|
510
|
+
stream: false,
|
|
511
|
+
};
|
|
512
|
+
/** `createEndpoint(...)` anywhere in the expression, through wrappers. */
|
|
513
|
+
function callsCreateEndpoint(node) {
|
|
514
|
+
if (!node || typeof node !== "object")
|
|
515
|
+
return false;
|
|
516
|
+
const n = node;
|
|
517
|
+
if (n.type === "CallExpression" &&
|
|
518
|
+
n.callee?.name ===
|
|
519
|
+
"createEndpoint") {
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
return Object.values(n).some((v) => Array.isArray(v) ? v.some(callsCreateEndpoint) : callsCreateEndpoint(v));
|
|
523
|
+
}
|
|
524
|
+
/** Unwrap `x satisfies T` / `x as const` to the expression underneath. */
|
|
525
|
+
function unwrapExpression(node) {
|
|
526
|
+
let current = node;
|
|
527
|
+
while ((current?.type === "TSAsExpression" ||
|
|
528
|
+
current?.type === "TSSatisfiesExpression" ||
|
|
529
|
+
current?.type === "TSNonNullExpression" ||
|
|
530
|
+
current?.type === "ParenthesizedExpression") &&
|
|
531
|
+
current.expression) {
|
|
532
|
+
current = current.expression;
|
|
533
|
+
}
|
|
534
|
+
return current;
|
|
535
|
+
}
|
|
486
536
|
function inspectEndpointFile(source) {
|
|
537
|
+
let ast;
|
|
487
538
|
try {
|
|
488
|
-
|
|
489
|
-
sourceType: "module",
|
|
490
|
-
plugins: ["typescript"],
|
|
491
|
-
});
|
|
492
|
-
const defaultExport = ast.program.body.find((n) => n.type === "ExportDefaultDeclaration" &&
|
|
493
|
-
n.declaration.type === "CallExpression");
|
|
494
|
-
if (!defaultExport || defaultExport.type !== "ExportDefaultDeclaration")
|
|
495
|
-
return { isEndpoint: false, stream: false };
|
|
496
|
-
const call = defaultExport.declaration;
|
|
497
|
-
if (call.type !== "CallExpression" || call.arguments.length === 0)
|
|
498
|
-
return { isEndpoint: false, stream: false };
|
|
499
|
-
const arg = call.arguments[0];
|
|
500
|
-
// `createEndpoint(config)` — anything else defaulting out of src/api/ is
|
|
501
|
-
// not an endpoint we can type a caller for.
|
|
502
|
-
if (arg.type !== "ObjectExpression")
|
|
503
|
-
return { isEndpoint: false, stream: false };
|
|
504
|
-
const streamProp = arg.properties.find((p) => p.type === "ObjectProperty" &&
|
|
505
|
-
((p.key.type === "Identifier" && p.key.name === "stream") ||
|
|
506
|
-
(p.key.type === "StringLiteral" && p.key.value === "stream")));
|
|
507
|
-
const stream = !!streamProp &&
|
|
508
|
-
streamProp.type === "ObjectProperty" &&
|
|
509
|
-
streamProp.value.type === "BooleanLiteral" &&
|
|
510
|
-
streamProp.value.value === true;
|
|
511
|
-
return { isEndpoint: true, stream };
|
|
539
|
+
ast = (0, parser_1.parse)(source, { sourceType: "module", plugins: ["typescript"] });
|
|
512
540
|
}
|
|
513
541
|
catch {
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
|
|
542
|
+
// Unparseable with a TS-only plugin set — most likely TSX, which is still
|
|
543
|
+
// a real endpoint file. Assume yes and let the app's own typecheck judge
|
|
544
|
+
// it, rather than dropping a route the bundler will deploy regardless.
|
|
545
|
+
return { isEndpoint: true, hasDefaultExport: true, stream: false };
|
|
517
546
|
}
|
|
547
|
+
const body = ast.program.body;
|
|
548
|
+
const hasDefaultExport = body.some((n) => n.type === "ExportDefaultDeclaration" ||
|
|
549
|
+
(n.type === "ExportNamedDeclaration" &&
|
|
550
|
+
n.specifiers?.some((spec) => spec.type === "ExportSpecifier" &&
|
|
551
|
+
(spec.exported.type === "Identifier"
|
|
552
|
+
? spec.exported.name
|
|
553
|
+
: spec.exported.value) === "default")));
|
|
554
|
+
const isEndpoint = hasDefaultExport || body.some(callsCreateEndpoint);
|
|
555
|
+
if (!isEndpoint)
|
|
556
|
+
return NOT_AN_ENDPOINT;
|
|
557
|
+
// Stream detection is best-effort on the literal config: it is the only
|
|
558
|
+
// shape we can read `stream: true` out of statically.
|
|
559
|
+
const defaultExport = body.find((n) => n.type === "ExportDefaultDeclaration");
|
|
560
|
+
const call = defaultExport
|
|
561
|
+
? unwrapExpression(defaultExport.declaration)
|
|
562
|
+
: undefined;
|
|
563
|
+
const arg = call?.type === "CallExpression"
|
|
564
|
+
? unwrapExpression(call.arguments?.[0] ?? {})
|
|
565
|
+
: undefined;
|
|
566
|
+
if (arg?.type !== "ObjectExpression") {
|
|
567
|
+
return { isEndpoint, hasDefaultExport, stream: false };
|
|
568
|
+
}
|
|
569
|
+
const streamProp = arg.properties.find((prop) => prop.type === "ObjectProperty" &&
|
|
570
|
+
((prop.key?.type === "Identifier" && prop.key.name === "stream") ||
|
|
571
|
+
(prop.key?.type === "StringLiteral" && prop.key.value === "stream")));
|
|
572
|
+
const stream = streamProp?.value?.type === "BooleanLiteral" &&
|
|
573
|
+
streamProp.value.value === true;
|
|
574
|
+
return { isEndpoint, hasDefaultExport, stream };
|
|
518
575
|
}
|
|
519
576
|
/**
|
|
520
577
|
* Reserved words can't be `const` names. The route is the filename either way,
|
|
@@ -556,8 +613,9 @@ function generateApiTs(endpointFiles) {
|
|
|
556
613
|
const ident = toSafeIdentifier(key);
|
|
557
614
|
// Distinct files can collide on one identifier (`send-email.ts` and
|
|
558
615
|
// `send_email.ts` both camelCase to `sendEmail`), which used to emit the
|
|
559
|
-
// same `export const` twice.
|
|
560
|
-
//
|
|
616
|
+
// same `export const` twice. First-wins, which is stable because every
|
|
617
|
+
// caller sorts its input before calling — the two in this package and
|
|
618
|
+
// `generateApiClient` in the backend's migration assembler.
|
|
561
619
|
if (seenIdents.has(ident)) {
|
|
562
620
|
skipped.push(`${fileName} (name collides with '${ident}')`);
|
|
563
621
|
continue;
|
|
@@ -569,6 +627,9 @@ function generateApiTs(endpointFiles) {
|
|
|
569
627
|
ident,
|
|
570
628
|
pascal: toPascalCase(key),
|
|
571
629
|
stream: shape?.stream ?? false,
|
|
630
|
+
// No content to inspect means a bare-filename caller, which historically
|
|
631
|
+
// assumed a default export — keep that assumption.
|
|
632
|
+
typed: shape ? shape.hasDefaultExport : true,
|
|
572
633
|
});
|
|
573
634
|
}
|
|
574
635
|
if (endpoints.length === 0)
|
|
@@ -588,11 +649,24 @@ function generateApiTs(endpointFiles) {
|
|
|
588
649
|
lines.push(`// ${name}`);
|
|
589
650
|
lines.push("");
|
|
590
651
|
}
|
|
591
|
-
for (const { pascal, baseName } of endpoints) {
|
|
652
|
+
for (const { pascal, baseName, typed } of endpoints) {
|
|
653
|
+
if (!typed)
|
|
654
|
+
continue;
|
|
592
655
|
lines.push(`import type { default as _${pascal}Ep } from '../src/api/${baseName}';`);
|
|
593
656
|
}
|
|
594
657
|
lines.push("");
|
|
595
|
-
for (const { baseName, ident, pascal, stream } of endpoints) {
|
|
658
|
+
for (const { baseName, ident, pascal, stream, typed } of endpoints) {
|
|
659
|
+
if (!typed) {
|
|
660
|
+
// Declared with `export const x = createEndpoint(...)` — the 1.0 shape.
|
|
661
|
+
// The route is real and the bundler deploys it, so the caller has to
|
|
662
|
+
// exist; there is just no default export to read its types from.
|
|
663
|
+
lines.push(`// '${baseName}' has no default export, so its input/output are untyped.`);
|
|
664
|
+
lines.push(`export type ${pascal}InputType = unknown;`);
|
|
665
|
+
lines.push(`export type ${pascal}OutputType = unknown;`);
|
|
666
|
+
lines.push(`export const ${ident} = ${stream ? "createStreamingCaller" : "createCaller"}<${pascal}InputType, ${pascal}OutputType>('${baseName}');`);
|
|
667
|
+
lines.push("");
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
596
670
|
lines.push(`type _${pascal}Cfg = typeof _${pascal}Ep;`);
|
|
597
671
|
// A caller sends the schema's INPUT type, not its output: a field with a
|
|
598
672
|
// `.default()` or a transform is optional to send and guaranteed on the
|
|
@@ -839,12 +913,14 @@ function generateAirtableTs(lock) {
|
|
|
839
913
|
"// Fields marked \"Links to X\" hold Airtable record ids (rec...) from",
|
|
840
914
|
"// findOne/findAll — never invent one.",
|
|
841
915
|
"// Read-only fields are absent from the *RecordInput types: Airtable rejects a",
|
|
842
|
-
"// write to a formula, rollup, lookup
|
|
916
|
+
"// write to a computed one (formula, rollup, lookup, autonumber, the created/",
|
|
917
|
+
"// modified metadata, button, aiText) with a 422.",
|
|
843
918
|
"//",
|
|
844
919
|
"// Usage tips:",
|
|
845
920
|
"// - Airtable has a strict rate limit of 5 requests/second per base",
|
|
846
|
-
"// - Use bulkCreate() instead of calling create() in a loop
|
|
847
|
-
"//
|
|
921
|
+
"// - Use bulkCreate() instead of calling create() in a loop, but chunk it",
|
|
922
|
+
"// yourself: Airtable rejects more than 10 records in one create request",
|
|
923
|
+
"// with a 422, and not every runtime path batches for you",
|
|
848
924
|
"// - findAll() offsets are opaque cursor strings from a previous call, NOT row",
|
|
849
925
|
"// counts (unlike zite.<table>.findAll, whose offset IS a number)",
|
|
850
926
|
"// - Always destructure record properties individually in create/update calls",
|
|
@@ -906,13 +982,31 @@ function generateBackendWrapperTs(envVarNames = []) {
|
|
|
906
982
|
"import type { User } from 'zitejs/auth';",
|
|
907
983
|
"",
|
|
908
984
|
"export type { ZiteErrorCode, ZiteSchedule, ZiteScheduledContext, ZiteStreamInterface, ZiteWebhook };",
|
|
985
|
+
// The pre-monorepo SDK put this in scope for every endpoint, so migrated
|
|
986
|
+
// code can name it. `createEndpoint` infers the same thing without it.
|
|
987
|
+
"export type InferSchemaType<T> = T extends { _output: infer U } ? U : T;",
|
|
909
988
|
"",
|
|
910
989
|
"export class ZiteError extends Error {",
|
|
911
990
|
" code: ZiteErrorCode;",
|
|
912
|
-
"
|
|
913
|
-
"
|
|
991
|
+
" /** Short, non-technical message suitable for showing to an end user. */",
|
|
992
|
+
" userFacingMessage?: string;",
|
|
993
|
+
" // Both shapes, mirroring the worker's own class: object form preferred,",
|
|
994
|
+
" // positional form is what pre-monorepo app code was written against.",
|
|
995
|
+
" constructor(options: { code: ZiteErrorCode; message: string; userFacingMessage?: string });",
|
|
996
|
+
" constructor(message: string, code?: ZiteErrorCode);",
|
|
997
|
+
" constructor(",
|
|
998
|
+
" optionsOrMessage: { code: ZiteErrorCode; message: string; userFacingMessage?: string } | string,",
|
|
999
|
+
" legacyCode?: ZiteErrorCode,",
|
|
1000
|
+
" ) {",
|
|
1001
|
+
" if (typeof optionsOrMessage === 'string') {",
|
|
1002
|
+
" super(optionsOrMessage);",
|
|
1003
|
+
" this.code = legacyCode ?? 'INTERNAL_ERROR';",
|
|
1004
|
+
" } else {",
|
|
1005
|
+
" super(optionsOrMessage.message);",
|
|
1006
|
+
" this.code = optionsOrMessage.code;",
|
|
1007
|
+
" this.userFacingMessage = optionsOrMessage.userFacingMessage;",
|
|
1008
|
+
" }",
|
|
914
1009
|
" this.name = 'ZiteError';",
|
|
915
|
-
" this.code = options.code;",
|
|
916
1010
|
" }",
|
|
917
1011
|
"}",
|
|
918
1012
|
"",
|
|
@@ -928,7 +1022,7 @@ function generateBackendWrapperTs(envVarNames = []) {
|
|
|
928
1022
|
// TStream mirrors zitejs/backend/base. Without it `stream: true` endpoints
|
|
929
1023
|
// get no `stream` argument here — and this wrapper, not the base module, is
|
|
930
1024
|
// what `zitejs/backend` resolves to in every app.
|
|
931
|
-
"export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TRawInput = TInput> {",
|
|
1025
|
+
"export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput> {",
|
|
932
1026
|
" description?: string;",
|
|
933
1027
|
" inputSchema?: SchemaLike<TInput, TRawInput>;",
|
|
934
1028
|
" outputSchema?: SchemaLike<TOutput>;",
|
|
@@ -936,18 +1030,19 @@ function generateBackendWrapperTs(envVarNames = []) {
|
|
|
936
1030
|
" authenticated?: boolean;",
|
|
937
1031
|
" /** When set, the endpoint also fires on this cron schedule. It stays request-callable — declaring one widens `context`, so `context.user` must be null-checked. */",
|
|
938
1032
|
" schedule?: TSchedule;",
|
|
939
|
-
" webhook
|
|
1033
|
+
" /** When set, an inbound webhook can also trigger this endpoint. Like `schedule`, it widens `context` — a webhook fire has no session. */",
|
|
1034
|
+
" webhook?: TWebhook;",
|
|
940
1035
|
" execute: (",
|
|
941
1036
|
" params: {",
|
|
942
1037
|
" input: TInput;",
|
|
943
|
-
" context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;",
|
|
1038
|
+
" context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;",
|
|
944
1039
|
" } & (TStream extends true ? { stream: ZiteStreamInterface } : {}),",
|
|
945
1040
|
" ) => Promise<TOutput> | TOutput;",
|
|
946
1041
|
"}",
|
|
947
1042
|
"",
|
|
948
|
-
"export function createEndpoint<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TRawInput = TInput>(",
|
|
949
|
-
" config: EndpointConfig<TInput, TOutput, TStream, TSchedule, TRawInput>,",
|
|
950
|
-
"): EndpointConfig<TInput, TOutput, TStream, TSchedule, TRawInput> {",
|
|
1043
|
+
"export function createEndpoint<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput>(",
|
|
1044
|
+
" config: EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput>,",
|
|
1045
|
+
"): EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput> {",
|
|
951
1046
|
" return config;",
|
|
952
1047
|
"}",
|
|
953
1048
|
"",
|
|
@@ -27,13 +27,22 @@ export type ZiteErrorCode = "BAD_REQUEST" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_
|
|
|
27
27
|
* through a fixed table (BAD_REQUEST -> 400, and so on); it reads `code` and
|
|
28
28
|
* nothing else, so an error carrying only a numeric `statusCode` fell through
|
|
29
29
|
* to a generic 500.
|
|
30
|
+
*
|
|
31
|
+
* Both call shapes, mirroring the worker's own class (`zite-runtime.ts`): the
|
|
32
|
+
* object form is preferred, and the positional form is what pre-monorepo app
|
|
33
|
+
* code was written against. Typing only the object form would have failed the
|
|
34
|
+
* typecheck of every migrated app that throws one.
|
|
30
35
|
*/
|
|
31
36
|
export declare class ZiteError extends Error {
|
|
32
37
|
code: ZiteErrorCode;
|
|
38
|
+
/** Short, non-technical message suitable for showing to an end user. */
|
|
39
|
+
userFacingMessage?: string;
|
|
33
40
|
constructor(options: {
|
|
34
41
|
code: ZiteErrorCode;
|
|
35
42
|
message: string;
|
|
43
|
+
userFacingMessage?: string;
|
|
36
44
|
});
|
|
45
|
+
constructor(message: string, code?: ZiteErrorCode);
|
|
37
46
|
}
|
|
38
47
|
/**
|
|
39
48
|
* Structurally matches a zod schema. `TIn` is the schema's *input* type, which
|
|
@@ -49,7 +58,7 @@ export type ZiteStreamInterface = {
|
|
|
49
58
|
write: (data: unknown) => Promise<void>;
|
|
50
59
|
forward: (asyncIterable: AsyncIterable<string>) => Promise<string>;
|
|
51
60
|
};
|
|
52
|
-
export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TRawInput = TInput> {
|
|
61
|
+
export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput> {
|
|
53
62
|
description?: string;
|
|
54
63
|
inputSchema?: SchemaLike<TInput, TRawInput>;
|
|
55
64
|
outputSchema?: SchemaLike<TOutput>;
|
|
@@ -61,12 +70,17 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
|
|
|
61
70
|
* Declaring one widens `context` so `context.user` must be null-checked.
|
|
62
71
|
*/
|
|
63
72
|
schedule?: TSchedule;
|
|
64
|
-
|
|
73
|
+
/**
|
|
74
|
+
* When set, the endpoint can also be triggered by an inbound webhook. Like
|
|
75
|
+
* `schedule`, this widens `context` — the workflow-runner sends
|
|
76
|
+
* `{ user: null }` for a webhook fire exactly as it does for a cron one.
|
|
77
|
+
*/
|
|
78
|
+
webhook?: TWebhook;
|
|
65
79
|
execute: (params: {
|
|
66
80
|
input: TInput;
|
|
67
|
-
context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;
|
|
81
|
+
context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;
|
|
68
82
|
} & (TStream extends true ? {
|
|
69
83
|
stream: ZiteStreamInterface;
|
|
70
84
|
} : {})) => Promise<TOutput> | TOutput;
|
|
71
85
|
}
|
|
72
|
-
export declare function createEndpoint<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TRawInput = TInput>(config: EndpointConfig<TInput, TOutput, TStream, TSchedule, TRawInput>): EndpointConfig<TInput, TOutput, TStream, TSchedule, TRawInput>;
|
|
86
|
+
export declare function createEndpoint<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput>(config: EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput>): EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput>;
|
|
@@ -3,13 +3,27 @@
|
|
|
3
3
|
* through a fixed table (BAD_REQUEST -> 400, and so on); it reads `code` and
|
|
4
4
|
* nothing else, so an error carrying only a numeric `statusCode` fell through
|
|
5
5
|
* to a generic 500.
|
|
6
|
+
*
|
|
7
|
+
* Both call shapes, mirroring the worker's own class (`zite-runtime.ts`): the
|
|
8
|
+
* object form is preferred, and the positional form is what pre-monorepo app
|
|
9
|
+
* code was written against. Typing only the object form would have failed the
|
|
10
|
+
* typecheck of every migrated app that throws one.
|
|
6
11
|
*/
|
|
7
12
|
export class ZiteError extends Error {
|
|
8
13
|
code;
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
/** Short, non-technical message suitable for showing to an end user. */
|
|
15
|
+
userFacingMessage;
|
|
16
|
+
constructor(optionsOrMessage, legacyCode) {
|
|
17
|
+
if (typeof optionsOrMessage === "string") {
|
|
18
|
+
super(optionsOrMessage);
|
|
19
|
+
this.code = legacyCode ?? "INTERNAL_ERROR";
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
super(optionsOrMessage.message);
|
|
23
|
+
this.code = optionsOrMessage.code;
|
|
24
|
+
this.userFacingMessage = optionsOrMessage.userFacingMessage;
|
|
25
|
+
}
|
|
11
26
|
this.name = "ZiteError";
|
|
12
|
-
this.code = options.code;
|
|
13
27
|
}
|
|
14
28
|
}
|
|
15
29
|
export function createEndpoint(config) {
|
package/dist/esm/bundle/index.js
CHANGED
|
@@ -563,8 +563,13 @@ export async function runBundle() {
|
|
|
563
563
|
if (appFlag !== -1 && args[appFlag + 1]) {
|
|
564
564
|
baseDir = path.resolve(baseDir, 'apps', args[appFlag + 1]);
|
|
565
565
|
}
|
|
566
|
-
// If explicit endpoint names passed, use those; otherwise find all
|
|
567
|
-
|
|
566
|
+
// If explicit endpoint names passed, use those; otherwise find all.
|
|
567
|
+
// Skipping `--app`'s VALUE as well as the flag: it is a positional arg, so
|
|
568
|
+
// filtering on the `--` prefix alone treated the app directory name as an
|
|
569
|
+
// endpoint name — `zitejs bundle --app myapp` tried to bundle
|
|
570
|
+
// `src/api/myapp.ts`, found nothing else, and reported every real endpoint
|
|
571
|
+
// as missing.
|
|
572
|
+
const explicitNames = args.filter((a, i) => !a.startsWith('--') && !(appFlag !== -1 && i === appFlag + 1));
|
|
568
573
|
let endpointNames;
|
|
569
574
|
if (explicitNames.length > 0) {
|
|
570
575
|
endpointNames = explicitNames;
|
|
@@ -2,21 +2,30 @@ import type { NotificationsCreateParams, NotificationsCreateResult } from "../no
|
|
|
2
2
|
/**
|
|
3
3
|
* A comparison against one field. Mirrors base-runner's operator set
|
|
4
4
|
* (`LegacyWhereConditionOperators`); anything else in the object is ignored.
|
|
5
|
+
*
|
|
6
|
+
* The operand types deliberately match the pre-monorepo `FilterOperators`
|
|
7
|
+
* rather than tightening to the field's own type. The value of typing filters
|
|
8
|
+
* is in the KEY — an unknown key silently returns every row — and tightening
|
|
9
|
+
* the operands buys little while breaking real migrated code: dates are ISO
|
|
10
|
+
* strings here but 1.0 accepted `number | Date` on every comparison, and
|
|
11
|
+
* `contains` was `any`.
|
|
5
12
|
*/
|
|
6
13
|
export type FilterCondition<V> = {
|
|
7
14
|
/** Substring match (text fields) or "has any of these" (linked records). */
|
|
8
|
-
contains?:
|
|
15
|
+
contains?: any;
|
|
9
16
|
/** Not equal, or — against `null` — "is set". */
|
|
10
17
|
not?: V | null;
|
|
11
18
|
/** Empty array means "no filter", not "match nothing". */
|
|
12
19
|
in?: V extends Array<infer E> ? E[] : V[];
|
|
13
20
|
/** Empty array means "no filter", not "match everything". */
|
|
14
21
|
notIn?: V extends Array<infer E> ? E[] : V[];
|
|
15
|
-
lt?: V;
|
|
16
|
-
lte?: V;
|
|
17
|
-
gt?: V;
|
|
18
|
-
gte?: V;
|
|
22
|
+
lt?: V | number | Date;
|
|
23
|
+
lte?: V | number | Date;
|
|
24
|
+
gt?: V | number | Date;
|
|
25
|
+
gte?: V | number | Date;
|
|
19
26
|
};
|
|
27
|
+
/** Pre-monorepo name for {@link FilterCondition}. Kept so migrated app code that spells the type out still compiles. */
|
|
28
|
+
export type FilterOperators<V> = FilterCondition<V> | V;
|
|
20
29
|
/**
|
|
21
30
|
* Filters keyed by the record's own field names. Typed rather than `unknown`
|
|
22
31
|
* because an unrecognized key is not an error at runtime — it passes through
|
|
@@ -24,8 +33,16 @@ export type FilterCondition<V> = {
|
|
|
24
33
|
* comes back **unfiltered**. A typo silently returns every row.
|
|
25
34
|
*/
|
|
26
35
|
export type RecordFilters<T> = {
|
|
27
|
-
[K in keyof T]?: T[K] | FilterCondition<T[K]>;
|
|
36
|
+
[K in keyof T]?: T[K] | (T[K] extends Array<infer E> ? E : never) | FilterCondition<T[K]>;
|
|
28
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* Resolves a zod schema to the type it produces. `createEndpoint` infers this
|
|
40
|
+
* for you, so it is rarely needed — it exists because the pre-monorepo SDK
|
|
41
|
+
* exposed it and migrated app code may still name it.
|
|
42
|
+
*/
|
|
43
|
+
export type InferSchemaType<T> = T extends {
|
|
44
|
+
_output: infer U;
|
|
45
|
+
} ? U : T;
|
|
29
46
|
export interface TableFindAllOptions<T = Record<string, unknown>> {
|
|
30
47
|
limit?: number;
|
|
31
48
|
offset?: number;
|
package/dist/esm/sync/lib.js
CHANGED
|
@@ -42,10 +42,15 @@ const FIELD_INPUT_TYPE_MAP = {
|
|
|
42
42
|
user: "string | string[] | null",
|
|
43
43
|
};
|
|
44
44
|
/**
|
|
45
|
-
* Computed and system-managed fields
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
45
|
+
* Computed and system-managed fields, omitted from the generated input type
|
|
46
|
+
* rather than typed and then refused at runtime.
|
|
47
|
+
*
|
|
48
|
+
* Mirrors `COMPUTED_FIELD_TYPES` in the product's own `shared/bases/fields`.
|
|
49
|
+
* The first six are rejected by `createRecordDataSchema` — each has a
|
|
50
|
+
* `refine(val => val == null)`, so a non-null write errors (null is a no-op
|
|
51
|
+
* clear). `created_at` / `updated_at` are refused differently: they have no
|
|
52
|
+
* column at all, so a write is silently dropped, which is the worse outcome to
|
|
53
|
+
* leave typed as writable.
|
|
49
54
|
*/
|
|
50
55
|
const READ_ONLY_FIELD_TYPES = new Set([
|
|
51
56
|
"lookup",
|
|
@@ -54,6 +59,8 @@ const READ_ONLY_FIELD_TYPES = new Set([
|
|
|
54
59
|
"formula",
|
|
55
60
|
"autonumber",
|
|
56
61
|
"updated_by",
|
|
62
|
+
"created_at",
|
|
63
|
+
"updated_at",
|
|
57
64
|
]);
|
|
58
65
|
/**
|
|
59
66
|
* Emitted once per `db.ts`. The old inline `{ url: string; name?: string }` was
|
|
@@ -100,6 +107,19 @@ const DURATION_FORMAT_EXAMPLES = {
|
|
|
100
107
|
"h:mm:ss.ss": "1:23:03.00",
|
|
101
108
|
"h:mm:ss.sss": "1:23:03.000",
|
|
102
109
|
};
|
|
110
|
+
// Naming the format alone ("display as `european` format") doesn't say what to
|
|
111
|
+
// render — these mirror the pickers in the database UI, which is what the user
|
|
112
|
+
// chose from. `local` is the viewer's system locale, so it has no fixed example.
|
|
113
|
+
const DATE_FORMAT_EXAMPLES = {
|
|
114
|
+
long: "January 15, 2024",
|
|
115
|
+
us: "1/15/2024",
|
|
116
|
+
european: "15/01/2024",
|
|
117
|
+
iso: "2024-01-15",
|
|
118
|
+
};
|
|
119
|
+
const withDateFormatExample = (format) => {
|
|
120
|
+
const example = DATE_FORMAT_EXAMPLES[format];
|
|
121
|
+
return example ? `"${format}" format (e.g. ${example})` : `"${format}" format`;
|
|
122
|
+
};
|
|
103
123
|
export function toPascalCase(name) {
|
|
104
124
|
const pascal = name
|
|
105
125
|
.replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase())
|
|
@@ -178,13 +198,13 @@ function fieldJsdoc(schemaField, table, schema) {
|
|
|
178
198
|
if (def.type === "date") {
|
|
179
199
|
const tpl = def.template;
|
|
180
200
|
if (tpl.dateFormat)
|
|
181
|
-
parts.push(`Date-only field (YYYY-MM-DD string), null when unset, display as
|
|
201
|
+
parts.push(`Date-only field (YYYY-MM-DD string), null when unset, display as ${withDateFormatExample(tpl.dateFormat)}`);
|
|
182
202
|
}
|
|
183
203
|
if (def.type === "datetime") {
|
|
184
204
|
const tpl = def.template;
|
|
185
205
|
const timeParts = ["Date+time field (ISO 8601 timestamp), null when unset"];
|
|
186
206
|
if (tpl.dateFormat)
|
|
187
|
-
timeParts.push(`date: ${tpl.dateFormat}`);
|
|
207
|
+
timeParts.push(`date: ${withDateFormatExample(tpl.dateFormat)}`);
|
|
188
208
|
if (tpl.timeFormat)
|
|
189
209
|
timeParts.push(`time: ${tpl.timeFormat}`);
|
|
190
210
|
if (tpl.timezone)
|
|
@@ -383,13 +403,13 @@ export function generateDbTs(schema) {
|
|
|
383
403
|
lines.push("// operators: contains, not, in, notIn, lt, lte, gt, gte");
|
|
384
404
|
lines.push('// `not: null` means "is set"; `in`/`notIn` with an empty array apply NO filter');
|
|
385
405
|
lines.push("// sort: [{ field: 'fieldName', direction: 'asc' | 'desc' }]");
|
|
386
|
-
lines.push("// limit defaults to 500, max
|
|
406
|
+
lines.push("// limit defaults to 500, max 2000; offset is a row count (a number)");
|
|
387
407
|
lines.push("// .findOne({ id?, filters?, fields? }) → T | undefined");
|
|
388
408
|
lines.push("// .create({ record }) → T");
|
|
389
409
|
lines.push("// .update({ id, record }) → { id: string, fields: Partial<T> }");
|
|
390
410
|
lines.push("// .delete({ id }) → { success: true, id: string }");
|
|
391
411
|
lines.push("// .bulkCreate({ records, matchOn? }) → { success: boolean, records: T[] }");
|
|
392
|
-
lines.push("// up to
|
|
412
|
+
lines.push("// up to 100 records per call; matchOn upserts on those fields");
|
|
393
413
|
lines.push("//");
|
|
394
414
|
lines.push("// Reading these types:");
|
|
395
415
|
lines.push("// SDK names are stable identifiers — they do NOT change when a table or field is");
|
|
@@ -400,8 +420,9 @@ export function generateDbTs(schema) {
|
|
|
400
420
|
lines.push("// display yourself, following the display config in each field's comment.");
|
|
401
421
|
lines.push("// PERCENT fields are stored as decimals — 0.5 is 50%, 1.0 is 100%.");
|
|
402
422
|
lines.push("// LINKED RECORD fields hold UUID record ids from findOne/findAll — never invent one.");
|
|
403
|
-
lines.push("// Computed fields (formula, rollup, lookup, autonumber
|
|
404
|
-
lines.push("//
|
|
423
|
+
lines.push("// Computed and system-managed fields (formula, rollup, lookup, autonumber,");
|
|
424
|
+
lines.push("// created/updated timestamps and more) are absent from the *Input types —");
|
|
425
|
+
lines.push("// a write to one is rejected or silently dropped.");
|
|
405
426
|
lines.push("//");
|
|
406
427
|
lines.push("// Zite auth users:");
|
|
407
428
|
lines.push("// zite.auth.findAllUsers({ appIds?, filter?, filters?, sort?, limit?, offset? })");
|
|
@@ -415,7 +436,7 @@ export function generateDbTs(schema) {
|
|
|
415
436
|
lines.push("// → { rows: Record<string, unknown>[], columns, rowCount, truncated }");
|
|
416
437
|
lines.push("// Capped at 2000 rows — `truncated: true` means there were more, so paginate in");
|
|
417
438
|
lines.push("// SQL rather than assuming you got everything. Queries time out after 10s.");
|
|
418
|
-
lines.push("// columns[].name is the SDK name; columns[].originalName is the
|
|
439
|
+
lines.push("// columns[].name is the SDK name; columns[].originalName is the underlying", "// database column (or your own SQL alias).");
|
|
419
440
|
lines.push("//");
|
|
420
441
|
lines.push("// SQL guidelines:");
|
|
421
442
|
lines.push("// - Use SDK names for tables (PascalCase) and fields (camelCase)");
|
|
@@ -473,38 +494,74 @@ export function generateDbTs(schema) {
|
|
|
473
494
|
lines.push("");
|
|
474
495
|
return lines.join("\n");
|
|
475
496
|
}
|
|
497
|
+
const NOT_AN_ENDPOINT = {
|
|
498
|
+
isEndpoint: false,
|
|
499
|
+
hasDefaultExport: false,
|
|
500
|
+
stream: false,
|
|
501
|
+
};
|
|
502
|
+
/** `createEndpoint(...)` anywhere in the expression, through wrappers. */
|
|
503
|
+
function callsCreateEndpoint(node) {
|
|
504
|
+
if (!node || typeof node !== "object")
|
|
505
|
+
return false;
|
|
506
|
+
const n = node;
|
|
507
|
+
if (n.type === "CallExpression" &&
|
|
508
|
+
n.callee?.name ===
|
|
509
|
+
"createEndpoint") {
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
return Object.values(n).some((v) => Array.isArray(v) ? v.some(callsCreateEndpoint) : callsCreateEndpoint(v));
|
|
513
|
+
}
|
|
514
|
+
/** Unwrap `x satisfies T` / `x as const` to the expression underneath. */
|
|
515
|
+
function unwrapExpression(node) {
|
|
516
|
+
let current = node;
|
|
517
|
+
while ((current?.type === "TSAsExpression" ||
|
|
518
|
+
current?.type === "TSSatisfiesExpression" ||
|
|
519
|
+
current?.type === "TSNonNullExpression" ||
|
|
520
|
+
current?.type === "ParenthesizedExpression") &&
|
|
521
|
+
current.expression) {
|
|
522
|
+
current = current.expression;
|
|
523
|
+
}
|
|
524
|
+
return current;
|
|
525
|
+
}
|
|
476
526
|
function inspectEndpointFile(source) {
|
|
527
|
+
let ast;
|
|
477
528
|
try {
|
|
478
|
-
|
|
479
|
-
sourceType: "module",
|
|
480
|
-
plugins: ["typescript"],
|
|
481
|
-
});
|
|
482
|
-
const defaultExport = ast.program.body.find((n) => n.type === "ExportDefaultDeclaration" &&
|
|
483
|
-
n.declaration.type === "CallExpression");
|
|
484
|
-
if (!defaultExport || defaultExport.type !== "ExportDefaultDeclaration")
|
|
485
|
-
return { isEndpoint: false, stream: false };
|
|
486
|
-
const call = defaultExport.declaration;
|
|
487
|
-
if (call.type !== "CallExpression" || call.arguments.length === 0)
|
|
488
|
-
return { isEndpoint: false, stream: false };
|
|
489
|
-
const arg = call.arguments[0];
|
|
490
|
-
// `createEndpoint(config)` — anything else defaulting out of src/api/ is
|
|
491
|
-
// not an endpoint we can type a caller for.
|
|
492
|
-
if (arg.type !== "ObjectExpression")
|
|
493
|
-
return { isEndpoint: false, stream: false };
|
|
494
|
-
const streamProp = arg.properties.find((p) => p.type === "ObjectProperty" &&
|
|
495
|
-
((p.key.type === "Identifier" && p.key.name === "stream") ||
|
|
496
|
-
(p.key.type === "StringLiteral" && p.key.value === "stream")));
|
|
497
|
-
const stream = !!streamProp &&
|
|
498
|
-
streamProp.type === "ObjectProperty" &&
|
|
499
|
-
streamProp.value.type === "BooleanLiteral" &&
|
|
500
|
-
streamProp.value.value === true;
|
|
501
|
-
return { isEndpoint: true, stream };
|
|
529
|
+
ast = parse(source, { sourceType: "module", plugins: ["typescript"] });
|
|
502
530
|
}
|
|
503
531
|
catch {
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
|
|
532
|
+
// Unparseable with a TS-only plugin set — most likely TSX, which is still
|
|
533
|
+
// a real endpoint file. Assume yes and let the app's own typecheck judge
|
|
534
|
+
// it, rather than dropping a route the bundler will deploy regardless.
|
|
535
|
+
return { isEndpoint: true, hasDefaultExport: true, stream: false };
|
|
507
536
|
}
|
|
537
|
+
const body = ast.program.body;
|
|
538
|
+
const hasDefaultExport = body.some((n) => n.type === "ExportDefaultDeclaration" ||
|
|
539
|
+
(n.type === "ExportNamedDeclaration" &&
|
|
540
|
+
n.specifiers?.some((spec) => spec.type === "ExportSpecifier" &&
|
|
541
|
+
(spec.exported.type === "Identifier"
|
|
542
|
+
? spec.exported.name
|
|
543
|
+
: spec.exported.value) === "default")));
|
|
544
|
+
const isEndpoint = hasDefaultExport || body.some(callsCreateEndpoint);
|
|
545
|
+
if (!isEndpoint)
|
|
546
|
+
return NOT_AN_ENDPOINT;
|
|
547
|
+
// Stream detection is best-effort on the literal config: it is the only
|
|
548
|
+
// shape we can read `stream: true` out of statically.
|
|
549
|
+
const defaultExport = body.find((n) => n.type === "ExportDefaultDeclaration");
|
|
550
|
+
const call = defaultExport
|
|
551
|
+
? unwrapExpression(defaultExport.declaration)
|
|
552
|
+
: undefined;
|
|
553
|
+
const arg = call?.type === "CallExpression"
|
|
554
|
+
? unwrapExpression(call.arguments?.[0] ?? {})
|
|
555
|
+
: undefined;
|
|
556
|
+
if (arg?.type !== "ObjectExpression") {
|
|
557
|
+
return { isEndpoint, hasDefaultExport, stream: false };
|
|
558
|
+
}
|
|
559
|
+
const streamProp = arg.properties.find((prop) => prop.type === "ObjectProperty" &&
|
|
560
|
+
((prop.key?.type === "Identifier" && prop.key.name === "stream") ||
|
|
561
|
+
(prop.key?.type === "StringLiteral" && prop.key.value === "stream")));
|
|
562
|
+
const stream = streamProp?.value?.type === "BooleanLiteral" &&
|
|
563
|
+
streamProp.value.value === true;
|
|
564
|
+
return { isEndpoint, hasDefaultExport, stream };
|
|
508
565
|
}
|
|
509
566
|
/**
|
|
510
567
|
* Reserved words can't be `const` names. The route is the filename either way,
|
|
@@ -546,8 +603,9 @@ export function generateApiTs(endpointFiles) {
|
|
|
546
603
|
const ident = toSafeIdentifier(key);
|
|
547
604
|
// Distinct files can collide on one identifier (`send-email.ts` and
|
|
548
605
|
// `send_email.ts` both camelCase to `sendEmail`), which used to emit the
|
|
549
|
-
// same `export const` twice.
|
|
550
|
-
//
|
|
606
|
+
// same `export const` twice. First-wins, which is stable because every
|
|
607
|
+
// caller sorts its input before calling — the two in this package and
|
|
608
|
+
// `generateApiClient` in the backend's migration assembler.
|
|
551
609
|
if (seenIdents.has(ident)) {
|
|
552
610
|
skipped.push(`${fileName} (name collides with '${ident}')`);
|
|
553
611
|
continue;
|
|
@@ -559,6 +617,9 @@ export function generateApiTs(endpointFiles) {
|
|
|
559
617
|
ident,
|
|
560
618
|
pascal: toPascalCase(key),
|
|
561
619
|
stream: shape?.stream ?? false,
|
|
620
|
+
// No content to inspect means a bare-filename caller, which historically
|
|
621
|
+
// assumed a default export — keep that assumption.
|
|
622
|
+
typed: shape ? shape.hasDefaultExport : true,
|
|
562
623
|
});
|
|
563
624
|
}
|
|
564
625
|
if (endpoints.length === 0)
|
|
@@ -578,11 +639,24 @@ export function generateApiTs(endpointFiles) {
|
|
|
578
639
|
lines.push(`// ${name}`);
|
|
579
640
|
lines.push("");
|
|
580
641
|
}
|
|
581
|
-
for (const { pascal, baseName } of endpoints) {
|
|
642
|
+
for (const { pascal, baseName, typed } of endpoints) {
|
|
643
|
+
if (!typed)
|
|
644
|
+
continue;
|
|
582
645
|
lines.push(`import type { default as _${pascal}Ep } from '../src/api/${baseName}';`);
|
|
583
646
|
}
|
|
584
647
|
lines.push("");
|
|
585
|
-
for (const { baseName, ident, pascal, stream } of endpoints) {
|
|
648
|
+
for (const { baseName, ident, pascal, stream, typed } of endpoints) {
|
|
649
|
+
if (!typed) {
|
|
650
|
+
// Declared with `export const x = createEndpoint(...)` — the 1.0 shape.
|
|
651
|
+
// The route is real and the bundler deploys it, so the caller has to
|
|
652
|
+
// exist; there is just no default export to read its types from.
|
|
653
|
+
lines.push(`// '${baseName}' has no default export, so its input/output are untyped.`);
|
|
654
|
+
lines.push(`export type ${pascal}InputType = unknown;`);
|
|
655
|
+
lines.push(`export type ${pascal}OutputType = unknown;`);
|
|
656
|
+
lines.push(`export const ${ident} = ${stream ? "createStreamingCaller" : "createCaller"}<${pascal}InputType, ${pascal}OutputType>('${baseName}');`);
|
|
657
|
+
lines.push("");
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
586
660
|
lines.push(`type _${pascal}Cfg = typeof _${pascal}Ep;`);
|
|
587
661
|
// A caller sends the schema's INPUT type, not its output: a field with a
|
|
588
662
|
// `.default()` or a transform is optional to send and guaranteed on the
|
|
@@ -829,12 +903,14 @@ export function generateAirtableTs(lock) {
|
|
|
829
903
|
"// Fields marked \"Links to X\" hold Airtable record ids (rec...) from",
|
|
830
904
|
"// findOne/findAll — never invent one.",
|
|
831
905
|
"// Read-only fields are absent from the *RecordInput types: Airtable rejects a",
|
|
832
|
-
"// write to a formula, rollup, lookup
|
|
906
|
+
"// write to a computed one (formula, rollup, lookup, autonumber, the created/",
|
|
907
|
+
"// modified metadata, button, aiText) with a 422.",
|
|
833
908
|
"//",
|
|
834
909
|
"// Usage tips:",
|
|
835
910
|
"// - Airtable has a strict rate limit of 5 requests/second per base",
|
|
836
|
-
"// - Use bulkCreate() instead of calling create() in a loop
|
|
837
|
-
"//
|
|
911
|
+
"// - Use bulkCreate() instead of calling create() in a loop, but chunk it",
|
|
912
|
+
"// yourself: Airtable rejects more than 10 records in one create request",
|
|
913
|
+
"// with a 422, and not every runtime path batches for you",
|
|
838
914
|
"// - findAll() offsets are opaque cursor strings from a previous call, NOT row",
|
|
839
915
|
"// counts (unlike zite.<table>.findAll, whose offset IS a number)",
|
|
840
916
|
"// - Always destructure record properties individually in create/update calls",
|
|
@@ -896,13 +972,31 @@ export function generateBackendWrapperTs(envVarNames = []) {
|
|
|
896
972
|
"import type { User } from 'zitejs/auth';",
|
|
897
973
|
"",
|
|
898
974
|
"export type { ZiteErrorCode, ZiteSchedule, ZiteScheduledContext, ZiteStreamInterface, ZiteWebhook };",
|
|
975
|
+
// The pre-monorepo SDK put this in scope for every endpoint, so migrated
|
|
976
|
+
// code can name it. `createEndpoint` infers the same thing without it.
|
|
977
|
+
"export type InferSchemaType<T> = T extends { _output: infer U } ? U : T;",
|
|
899
978
|
"",
|
|
900
979
|
"export class ZiteError extends Error {",
|
|
901
980
|
" code: ZiteErrorCode;",
|
|
902
|
-
"
|
|
903
|
-
"
|
|
981
|
+
" /** Short, non-technical message suitable for showing to an end user. */",
|
|
982
|
+
" userFacingMessage?: string;",
|
|
983
|
+
" // Both shapes, mirroring the worker's own class: object form preferred,",
|
|
984
|
+
" // positional form is what pre-monorepo app code was written against.",
|
|
985
|
+
" constructor(options: { code: ZiteErrorCode; message: string; userFacingMessage?: string });",
|
|
986
|
+
" constructor(message: string, code?: ZiteErrorCode);",
|
|
987
|
+
" constructor(",
|
|
988
|
+
" optionsOrMessage: { code: ZiteErrorCode; message: string; userFacingMessage?: string } | string,",
|
|
989
|
+
" legacyCode?: ZiteErrorCode,",
|
|
990
|
+
" ) {",
|
|
991
|
+
" if (typeof optionsOrMessage === 'string') {",
|
|
992
|
+
" super(optionsOrMessage);",
|
|
993
|
+
" this.code = legacyCode ?? 'INTERNAL_ERROR';",
|
|
994
|
+
" } else {",
|
|
995
|
+
" super(optionsOrMessage.message);",
|
|
996
|
+
" this.code = optionsOrMessage.code;",
|
|
997
|
+
" this.userFacingMessage = optionsOrMessage.userFacingMessage;",
|
|
998
|
+
" }",
|
|
904
999
|
" this.name = 'ZiteError';",
|
|
905
|
-
" this.code = options.code;",
|
|
906
1000
|
" }",
|
|
907
1001
|
"}",
|
|
908
1002
|
"",
|
|
@@ -918,7 +1012,7 @@ export function generateBackendWrapperTs(envVarNames = []) {
|
|
|
918
1012
|
// TStream mirrors zitejs/backend/base. Without it `stream: true` endpoints
|
|
919
1013
|
// get no `stream` argument here — and this wrapper, not the base module, is
|
|
920
1014
|
// what `zitejs/backend` resolves to in every app.
|
|
921
|
-
"export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TRawInput = TInput> {",
|
|
1015
|
+
"export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput> {",
|
|
922
1016
|
" description?: string;",
|
|
923
1017
|
" inputSchema?: SchemaLike<TInput, TRawInput>;",
|
|
924
1018
|
" outputSchema?: SchemaLike<TOutput>;",
|
|
@@ -926,18 +1020,19 @@ export function generateBackendWrapperTs(envVarNames = []) {
|
|
|
926
1020
|
" authenticated?: boolean;",
|
|
927
1021
|
" /** When set, the endpoint also fires on this cron schedule. It stays request-callable — declaring one widens `context`, so `context.user` must be null-checked. */",
|
|
928
1022
|
" schedule?: TSchedule;",
|
|
929
|
-
" webhook
|
|
1023
|
+
" /** When set, an inbound webhook can also trigger this endpoint. Like `schedule`, it widens `context` — a webhook fire has no session. */",
|
|
1024
|
+
" webhook?: TWebhook;",
|
|
930
1025
|
" execute: (",
|
|
931
1026
|
" params: {",
|
|
932
1027
|
" input: TInput;",
|
|
933
|
-
" context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;",
|
|
1028
|
+
" context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;",
|
|
934
1029
|
" } & (TStream extends true ? { stream: ZiteStreamInterface } : {}),",
|
|
935
1030
|
" ) => Promise<TOutput> | TOutput;",
|
|
936
1031
|
"}",
|
|
937
1032
|
"",
|
|
938
|
-
"export function createEndpoint<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TRawInput = TInput>(",
|
|
939
|
-
" config: EndpointConfig<TInput, TOutput, TStream, TSchedule, TRawInput>,",
|
|
940
|
-
"): EndpointConfig<TInput, TOutput, TStream, TSchedule, TRawInput> {",
|
|
1033
|
+
"export function createEndpoint<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput>(",
|
|
1034
|
+
" config: EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput>,",
|
|
1035
|
+
"): EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput> {",
|
|
941
1036
|
" return config;",
|
|
942
1037
|
"}",
|
|
943
1038
|
"",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zitejs",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "The Zite framework
|
|
3
|
+
"version": "0.9.96",
|
|
4
|
+
"description": "The Zite framework \u2014 build apps on Zite Database",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
7
7
|
"module": "./dist/esm/index.js",
|