lingcode-backend 0.1.0
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/index.d.ts +149 -0
- package/index.js +130 -0
- package/package.json +35 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Type definitions for lingcode-backend
|
|
2
|
+
|
|
3
|
+
declare const idBrand: unique symbol;
|
|
4
|
+
/** A row id branded with its table. At runtime it is a string. */
|
|
5
|
+
export type Id<TableName extends string> = string & { readonly [idBrand]: TableName };
|
|
6
|
+
|
|
7
|
+
export interface Validator<T, IsOptional extends boolean = false> {
|
|
8
|
+
readonly json: { type: string; [key: string]: unknown };
|
|
9
|
+
readonly isOptional: IsOptional;
|
|
10
|
+
/** Type-only marker. */
|
|
11
|
+
readonly __type?: T;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type Infer<V> = V extends Validator<infer T, any> ? T : never;
|
|
15
|
+
|
|
16
|
+
type OptionalKeys<F> = { [K in keyof F]: F[K] extends Validator<any, true> ? K : never }[keyof F];
|
|
17
|
+
type RequiredKeys<F> = Exclude<keyof F, OptionalKeys<F>>;
|
|
18
|
+
type Simplify<T> = { [K in keyof T]: T[K] } & {};
|
|
19
|
+
|
|
20
|
+
export type ObjectType<F extends Record<string, Validator<any, boolean>>> = Simplify<
|
|
21
|
+
{ [K in RequiredKeys<F>]: Infer<F[K]> } & { [K in OptionalKeys<F>]?: Infer<F[K]> }
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
export interface StringOptions { minLength?: number; maxLength?: number }
|
|
25
|
+
export interface NumberOptions { integer?: boolean; min?: number; max?: number }
|
|
26
|
+
|
|
27
|
+
export declare const v: {
|
|
28
|
+
string(opts?: StringOptions): Validator<string>;
|
|
29
|
+
number(opts?: NumberOptions): Validator<number>;
|
|
30
|
+
int(opts?: Omit<NumberOptions, 'integer'>): Validator<number>;
|
|
31
|
+
boolean(): Validator<boolean>;
|
|
32
|
+
null(): Validator<null>;
|
|
33
|
+
any(): Validator<any>;
|
|
34
|
+
id<TableName extends string>(table: TableName): Validator<Id<TableName>>;
|
|
35
|
+
/** An ISO 8601 date-time string. */
|
|
36
|
+
timestamp(): Validator<string>;
|
|
37
|
+
literal<T extends string | number | boolean | null>(value: T): Validator<T>;
|
|
38
|
+
array<T>(items: Validator<T>, opts?: { maxItems?: number }): Validator<T[]>;
|
|
39
|
+
object<F extends Record<string, Validator<any, boolean>>>(fields: F): Validator<ObjectType<F>>;
|
|
40
|
+
record<T>(values: Validator<T>): Validator<Record<string, T>>;
|
|
41
|
+
union<M extends Validator<any>[]>(...members: M): Validator<Infer<M[number]>>;
|
|
42
|
+
optional<T>(inner: Validator<T>): Validator<T, true>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export declare const string: typeof v.string;
|
|
46
|
+
export declare const number: typeof v.number;
|
|
47
|
+
export declare const int: typeof v.int;
|
|
48
|
+
export declare const boolean: typeof v.boolean;
|
|
49
|
+
export declare const nullValue: typeof v.null;
|
|
50
|
+
export declare const any: typeof v.any;
|
|
51
|
+
export declare const id: typeof v.id;
|
|
52
|
+
export declare const timestamp: typeof v.timestamp;
|
|
53
|
+
export declare const literal: typeof v.literal;
|
|
54
|
+
export declare const array: typeof v.array;
|
|
55
|
+
export declare const object: typeof v.object;
|
|
56
|
+
export declare const record: typeof v.record;
|
|
57
|
+
export declare const union: typeof v.union;
|
|
58
|
+
export declare const optional: typeof v.optional;
|
|
59
|
+
|
|
60
|
+
export interface QueryResult<Row = Record<string, any>> {
|
|
61
|
+
rows: Row[];
|
|
62
|
+
rowCount: number | null;
|
|
63
|
+
fields: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface DatabaseReader {
|
|
67
|
+
/** Run one parameterized SQL statement ($1, $2, …) inside the function's transaction. */
|
|
68
|
+
query<Row = Record<string, any>>(sql: string, params?: unknown[]): Promise<QueryResult<Row>>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface Auth {
|
|
72
|
+
/** The signed-in user's id, or null for an anonymous caller. */
|
|
73
|
+
userId: string | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface QueryCtx {
|
|
77
|
+
/** Read-only, repeatable-read snapshot. Writes fail. */
|
|
78
|
+
db: DatabaseReader;
|
|
79
|
+
auth: Auth;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface MutationCtx {
|
|
83
|
+
/** One serializable transaction for the whole handler. */
|
|
84
|
+
db: DatabaseReader;
|
|
85
|
+
auth: Auth;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ActionCtx {
|
|
89
|
+
auth: Auth;
|
|
90
|
+
/** Values of the secrets this action declares. */
|
|
91
|
+
secrets: Record<string, string>;
|
|
92
|
+
runQuery<F extends RegisteredQuery<any, any>>(ref: FunctionReference<'query', F>, args: FunctionArgs<F>): Promise<FunctionReturnType<F>>;
|
|
93
|
+
runMutation<F extends RegisteredMutation<any, any>>(ref: FunctionReference<'mutation', F>, args: FunctionArgs<F>): Promise<FunctionReturnType<F>>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type ArgsFor<A extends Record<string, Validator<any, boolean>>> = ObjectType<A>;
|
|
97
|
+
|
|
98
|
+
export interface RegisteredQuery<Args, Returns> {
|
|
99
|
+
readonly handler: (ctx: QueryCtx, args: Args) => Returns | Promise<Returns>;
|
|
100
|
+
readonly __kind?: 'query';
|
|
101
|
+
}
|
|
102
|
+
export interface RegisteredMutation<Args, Returns> {
|
|
103
|
+
readonly handler: (ctx: MutationCtx, args: Args) => Returns | Promise<Returns>;
|
|
104
|
+
readonly __kind?: 'mutation';
|
|
105
|
+
}
|
|
106
|
+
export interface RegisteredAction<Args, Returns> {
|
|
107
|
+
readonly handler: (ctx: ActionCtx, args: Args) => Returns | Promise<Returns>;
|
|
108
|
+
readonly __kind?: 'action';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type AnyRegistered = RegisteredQuery<any, any> | RegisteredMutation<any, any> | RegisteredAction<any, any>;
|
|
112
|
+
|
|
113
|
+
export type FunctionArgs<F> = F extends { handler: (ctx: any, args: infer A) => any } ? A : never;
|
|
114
|
+
export type FunctionReturnType<F> = F extends { handler: (ctx: any, args: any) => infer R } ? Awaited<R> : never;
|
|
115
|
+
|
|
116
|
+
/** A reference to a deployed function, from lingcode/_generated/api. */
|
|
117
|
+
export interface FunctionReference<Kind extends 'query' | 'mutation' | 'action', F extends AnyRegistered = AnyRegistered> {
|
|
118
|
+
readonly _path: string;
|
|
119
|
+
readonly _kind: Kind;
|
|
120
|
+
readonly __function?: F;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export declare function query<A extends Record<string, Validator<any, boolean>>, R>(definition: {
|
|
124
|
+
args: A;
|
|
125
|
+
returns?: Validator<R>;
|
|
126
|
+
handler: (ctx: QueryCtx, args: ArgsFor<A>) => R | Promise<R>;
|
|
127
|
+
}): RegisteredQuery<ArgsFor<A>, R> & { readonly __kind?: 'query' };
|
|
128
|
+
|
|
129
|
+
export declare function mutation<A extends Record<string, Validator<any, boolean>>, R>(definition: {
|
|
130
|
+
args: A;
|
|
131
|
+
returns?: Validator<R>;
|
|
132
|
+
handler: (ctx: MutationCtx, args: ArgsFor<A>) => R | Promise<R>;
|
|
133
|
+
}): RegisteredMutation<ArgsFor<A>, R> & { readonly __kind?: 'mutation' };
|
|
134
|
+
|
|
135
|
+
export declare function action<A extends Record<string, Validator<any, boolean>>, R>(definition: {
|
|
136
|
+
args: A;
|
|
137
|
+
returns?: Validator<R>;
|
|
138
|
+
/** Names of secrets this action may read through ctx.secrets. */
|
|
139
|
+
secrets?: string[];
|
|
140
|
+
handler: (ctx: ActionCtx, args: ArgsFor<A>) => R | Promise<R>;
|
|
141
|
+
}): RegisteredAction<ArgsFor<A>, R> & { readonly __kind?: 'action' };
|
|
142
|
+
|
|
143
|
+
export declare class LingCodeError<Data = unknown> extends Error {
|
|
144
|
+
constructor(code: string, options?: { message?: string; data?: Data });
|
|
145
|
+
readonly code: string;
|
|
146
|
+
readonly data: Data | null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export declare function isLingCodeError(error: unknown, code?: string): error is LingCodeError;
|
package/index.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// lingcode-backend — define named backend functions.
|
|
2
|
+
//
|
|
3
|
+
// This module is bundled into every deployed backend module, so it has no
|
|
4
|
+
// dependencies. Validators here only DESCRIBE values as JSON; the LingCode
|
|
5
|
+
// server enforces them (website/server/cloud-validators.js) before a handler
|
|
6
|
+
// runs and on the value it returns.
|
|
7
|
+
|
|
8
|
+
const MARK = '__lingcode';
|
|
9
|
+
|
|
10
|
+
class Validator {
|
|
11
|
+
constructor(json, optional = false) {
|
|
12
|
+
this.json = json;
|
|
13
|
+
this.isOptional = optional;
|
|
14
|
+
Object.freeze(this);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function validator(value, label) {
|
|
19
|
+
if (!(value instanceof Validator) && !(value && value.json && typeof value.json.type === 'string')) {
|
|
20
|
+
throw new TypeError(`${label} must be a validator such as v.string()`);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fieldsJson(fields, label) {
|
|
26
|
+
if (!fields || typeof fields !== 'object' || Array.isArray(fields)) throw new TypeError(`${label} must be an object of validators`);
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
29
|
+
const val = validator(value, `${label}.${key}`);
|
|
30
|
+
out[key] = val.isOptional ? { type: 'optional', inner: val.json } : val.json;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function numberOpts(opts) {
|
|
36
|
+
const out = {};
|
|
37
|
+
if (opts) {
|
|
38
|
+
if (opts.integer) out.integer = true;
|
|
39
|
+
if (opts.min !== undefined) out.min = opts.min;
|
|
40
|
+
if (opts.max !== undefined) out.max = opts.max;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const v = Object.freeze({
|
|
46
|
+
string: (opts) => new Validator({
|
|
47
|
+
type: 'string',
|
|
48
|
+
...(opts && opts.minLength !== undefined ? { minLength: opts.minLength } : {}),
|
|
49
|
+
...(opts && opts.maxLength !== undefined ? { maxLength: opts.maxLength } : {}),
|
|
50
|
+
}),
|
|
51
|
+
number: (opts) => new Validator({ type: 'number', ...numberOpts(opts) }),
|
|
52
|
+
int: (opts) => new Validator({ type: 'number', ...numberOpts(opts), integer: true }),
|
|
53
|
+
boolean: () => new Validator({ type: 'boolean' }),
|
|
54
|
+
null: () => new Validator({ type: 'null' }),
|
|
55
|
+
any: () => new Validator({ type: 'any' }),
|
|
56
|
+
id: (table) => {
|
|
57
|
+
if (typeof table !== 'string' || !table) throw new TypeError('v.id(table) needs a table name');
|
|
58
|
+
return new Validator({ type: 'id', table });
|
|
59
|
+
},
|
|
60
|
+
timestamp: () => new Validator({ type: 'timestamp' }),
|
|
61
|
+
literal: (value) => {
|
|
62
|
+
if (value !== null && !['string', 'number', 'boolean'].includes(typeof value)) {
|
|
63
|
+
throw new TypeError('v.literal() takes a string, number, boolean, or null');
|
|
64
|
+
}
|
|
65
|
+
return new Validator({ type: 'literal', value });
|
|
66
|
+
},
|
|
67
|
+
array: (items, opts) => new Validator({
|
|
68
|
+
type: 'array',
|
|
69
|
+
items: validator(items, 'v.array() items').json,
|
|
70
|
+
...(opts && opts.maxItems !== undefined ? { maxItems: opts.maxItems } : {}),
|
|
71
|
+
}),
|
|
72
|
+
object: (fields) => new Validator({ type: 'object', fields: fieldsJson(fields, 'v.object()') }),
|
|
73
|
+
record: (values) => new Validator({ type: 'record', values: validator(values, 'v.record() values').json }),
|
|
74
|
+
union: (...members) => {
|
|
75
|
+
if (!members.length) throw new TypeError('v.union() needs at least one member');
|
|
76
|
+
return new Validator({ type: 'union', members: members.map((m, i) => validator(m, `v.union() member ${i}`).json) });
|
|
77
|
+
},
|
|
78
|
+
optional: (inner) => new Validator(validator(inner, 'v.optional()').json, true),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export const {
|
|
82
|
+
string, number, int, boolean, any, id, timestamp, literal, array, object, record, union, optional,
|
|
83
|
+
} = v;
|
|
84
|
+
export const nullValue = v.null;
|
|
85
|
+
|
|
86
|
+
function define(kind, definition) {
|
|
87
|
+
if (!definition || typeof definition !== 'object') throw new TypeError(`${kind}() takes { args, handler }`);
|
|
88
|
+
if (typeof definition.handler !== 'function') throw new TypeError(`${kind}() needs a handler(ctx, args) function`);
|
|
89
|
+
if (!definition.args || typeof definition.args !== 'object') {
|
|
90
|
+
throw new TypeError(`${kind}() needs args: an object of validators (use args: {} for none)`);
|
|
91
|
+
}
|
|
92
|
+
const spec = {
|
|
93
|
+
kind,
|
|
94
|
+
args: fieldsJson(definition.args, `${kind}() args`),
|
|
95
|
+
returns: definition.returns ? validator(definition.returns, `${kind}() returns`).json : null,
|
|
96
|
+
};
|
|
97
|
+
if (kind === 'action') {
|
|
98
|
+
const secrets = definition.secrets === undefined ? [] : definition.secrets;
|
|
99
|
+
if (!Array.isArray(secrets) || secrets.some((s) => typeof s !== 'string' || !/^[A-Z][A-Z0-9_]{0,63}$/.test(s))) {
|
|
100
|
+
throw new TypeError('action() secrets must be an array of names like "STRIPE_SECRET_KEY"');
|
|
101
|
+
}
|
|
102
|
+
spec.secrets = [...new Set(secrets)];
|
|
103
|
+
} else if (definition.secrets !== undefined) {
|
|
104
|
+
throw new TypeError(`${kind}() can't read secrets; only actions can`);
|
|
105
|
+
}
|
|
106
|
+
const registered = { handler: definition.handler };
|
|
107
|
+
Object.defineProperty(registered, MARK, { value: Object.freeze(spec), enumerable: false });
|
|
108
|
+
return Object.freeze(registered);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const query = (definition) => define('query', definition);
|
|
112
|
+
export const mutation = (definition) => define('mutation', definition);
|
|
113
|
+
export const action = (definition) => define('action', definition);
|
|
114
|
+
|
|
115
|
+
// An error meant for the application: its code, message, and data reach the
|
|
116
|
+
// caller unchanged in production. Any other thrown error is reported as an
|
|
117
|
+
// internal error there, and its details stay in the backend logs.
|
|
118
|
+
export class LingCodeError extends Error {
|
|
119
|
+
constructor(code, { message, data } = {}) {
|
|
120
|
+
super(message || String(code));
|
|
121
|
+
this.name = 'LingCodeError';
|
|
122
|
+
this.code = String(code);
|
|
123
|
+
this.data = data === undefined ? null : data;
|
|
124
|
+
this.__lingcodeError = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function isLingCodeError(error, code) {
|
|
129
|
+
return !!(error && error.__lingcodeError === true && (code === undefined || error.code === code));
|
|
130
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lingcode-backend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Define LingCode Cloud queries, mutations, and actions with runtime-validated, typed arguments.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"default": "./index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"index.js",
|
|
17
|
+
"index.d.ts",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node --test test/*.test.mjs"
|
|
23
|
+
},
|
|
24
|
+
"keywords": ["lingcode", "backend", "query", "mutation", "reactive"],
|
|
25
|
+
"homepage": "https://lingcode.dev/docs/cloud/functions/queries.html",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/Xavierhuang/LingCode.git",
|
|
29
|
+
"directory": "packages/backend"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
}
|
|
35
|
+
}
|