@adrdp/pg-rest 0.1.3 → 0.2.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.
@@ -0,0 +1,146 @@
1
+ //#region src/schema.d.ts
2
+ interface Column {
3
+ readonly name: string;
4
+ /** Result of `format_type`, e.g. `integer`, `character varying(10)`, `text[]`. */
5
+ readonly type: string;
6
+ /**
7
+ * Labels of the enum type, in declaration order, when the column is an enum or an array of one.
8
+ * Domains over an enum are not unwrapped.
9
+ */
10
+ readonly enumLabels?: ReadonlyArray<string> | undefined;
11
+ /**
12
+ * For a view column read straight from a table column, that column -- followed through
13
+ * view-over-view chains. `nullable` still reports what the catalog says of the *view*, which is
14
+ * always true: a `NOT NULL` source says nothing about a `LEFT JOIN` in the view's own query.
15
+ */
16
+ readonly source?: {
17
+ readonly schema: string;
18
+ readonly table: string;
19
+ readonly column: string;
20
+ } | undefined;
21
+ readonly nullable: boolean;
22
+ readonly hasDefault: boolean;
23
+ /** SQL text of the default expression, when there is one. */
24
+ readonly defaultExpr: string | null;
25
+ readonly generated: boolean;
26
+ /**
27
+ * `GENERATED ALWAYS AS IDENTITY` or `GENERATED BY DEFAULT AS IDENTITY`. An `always` column refuses
28
+ * a value outright (`428C9`), unlike a column that merely has a default.
29
+ */
30
+ readonly identity?: "always" | "by-default" | undefined;
31
+ readonly comment: string | null;
32
+ readonly position: number;
33
+ }
34
+ type RelationKind = "table" | "view" | "matview" | "foreign" | "partitioned";
35
+ interface Relation {
36
+ readonly schema: string;
37
+ readonly name: string;
38
+ readonly kind: RelationKind;
39
+ readonly columns: ReadonlyArray<Column>;
40
+ readonly pk: ReadonlyArray<string>;
41
+ readonly uniques: ReadonlyArray<ReadonlyArray<string>>;
42
+ readonly insertable: boolean;
43
+ readonly updatable: boolean;
44
+ readonly deletable: boolean;
45
+ readonly comment: string | null;
46
+ }
47
+ interface ForeignKey {
48
+ readonly name: string;
49
+ readonly schema: string;
50
+ readonly table: string;
51
+ readonly cols: ReadonlyArray<string>;
52
+ readonly fschema: string;
53
+ readonly ftable: string;
54
+ readonly fcols: ReadonlyArray<string>;
55
+ }
56
+ interface FunctionArg {
57
+ readonly name: string;
58
+ readonly type: string;
59
+ readonly hasDefault: boolean;
60
+ readonly variadic: boolean;
61
+ /** Labels of the enum type, when the argument is an enum or an array of one. */
62
+ readonly enumLabels?: ReadonlyArray<string> | undefined;
63
+ }
64
+ interface FunctionDef {
65
+ readonly schema: string;
66
+ readonly name: string;
67
+ readonly args: ReadonlyArray<FunctionArg>;
68
+ readonly returnType: string;
69
+ /** Labels of the enum type, when the function returns an enum or an array of one. */
70
+ readonly returnEnumLabels?: ReadonlyArray<string> | undefined;
71
+ readonly returnsSet: boolean;
72
+ /** True if the function returns a composite / record / TABLE(...) row type. */
73
+ readonly returnsComposite: boolean;
74
+ readonly returnsVoid: boolean;
75
+ readonly volatility: "immutable" | "stable" | "volatile";
76
+ readonly comment: string | null;
77
+ /** The table when the function's only parameter is a table row (computed relationships). */
78
+ readonly argRelation?: {
79
+ readonly schema: string;
80
+ readonly name: string;
81
+ };
82
+ /** The table whose rows the function returns. */
83
+ readonly returnRelation?: {
84
+ readonly schema: string;
85
+ readonly name: string;
86
+ };
87
+ /** Planner row estimate; 1 on a set-returning function makes a computed relationship to-one. */
88
+ readonly rows: number;
89
+ /** Scalar functions returning a domain named like a media type (`"text/plain"`, …): the raw value is the body. */
90
+ readonly returnMediaType?: {
91
+ readonly name: string;
92
+ readonly bytea: boolean;
93
+ };
94
+ }
95
+ /** A custom media type handler: an aggregate named after the media type that returns a domain (see PostgREST's "custom media types"). */
96
+ interface MediaHandler {
97
+ readonly schema: string;
98
+ /** The aggregate's name, i.e. the media type (for example `text/plain`, or the catch-all). */
99
+ readonly mediaType: string;
100
+ /** The table whose rows it aggregates, or `undefined` for `anyelement`. */
101
+ readonly argRelation?: {
102
+ readonly schema: string;
103
+ readonly name: string;
104
+ };
105
+ /** True when the domain wraps `bytea` (the body is binary). */
106
+ readonly bytea: boolean;
107
+ }
108
+ interface SchemaCache {
109
+ readonly mediaHandlers: ReadonlyArray<MediaHandler>;
110
+ readonly schemas: ReadonlyArray<string>;
111
+ readonly relations: ReadonlyMap<string, Relation>;
112
+ readonly foreignKeys: ReadonlyArray<ForeignKey>;
113
+ readonly functions: ReadonlyMap<string, ReadonlyArray<FunctionDef>>;
114
+ }
115
+ //#endregion
116
+ //#region src/types.d.ts
117
+ /**
118
+ * Generate TypeScript types from an introspected schema, as the text of a module.
119
+ *
120
+ * The catalog is the only source: this is the same {@link SchemaCache} the request path reads, so
121
+ * what the types allow is what the library accepts. Write the result next to your code and keep it
122
+ * in version control:
123
+ *
124
+ * ```ts
125
+ * writeFileSync("src/db.types.ts", generateTypes(await pgRest.schemaCache()));
126
+ * ```
127
+ */
128
+ interface GenerateTypesOptions {
129
+ /** Name of the exported type. Default `"Database"`. */
130
+ readonly name?: string;
131
+ /**
132
+ * Nullability of view columns. The catalog reports every one of them as nullable, which is the
133
+ * truth as far as it knows: a `NOT NULL` source column says nothing about a `LEFT JOIN` in the
134
+ * view's own query. `"like-source"` follows `Column.source` instead -- shorter types, at the risk
135
+ * of claiming non-null where an outer join can produce one.
136
+ */
137
+ readonly views?: "nullable" | "like-source";
138
+ /** Banner comment. `false` leaves it out. */
139
+ readonly header?: string | false;
140
+ }
141
+ /** The type of one column's value, enum labels and array dimensions included. */
142
+ declare const columnType: (col: Pick<Column, "type" | "enumLabels">) => string;
143
+ /** Generate the module text. Deterministic: same catalog, same bytes. */
144
+ declare const generateTypes: (cache: SchemaCache, options?: GenerateTypesOptions) => string;
145
+ //#endregion
146
+ export { ForeignKey as a, SchemaCache as c, Column as i, columnType as n, FunctionDef as o, generateTypes as r, Relation as s, GenerateTypesOptions as t };
@@ -0,0 +1,2 @@
1
+ import { n as columnType, r as generateTypes, t as GenerateTypesOptions } from "./types-DaNxiC7H.js";
2
+ export { GenerateTypesOptions, columnType, generateTypes };
package/dist/types.js ADDED
@@ -0,0 +1,202 @@
1
+ import { a as relKey, t as findRelationships } from "./schema-BhF_TQdX.js";
2
+
3
+ //#region src/types.ts
4
+ const RESERVED_KEY = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
5
+ /** An object key: bare when it is a plain identifier, quoted otherwise. */
6
+ const key = (name) => RESERVED_KEY.test(name) ? name : JSON.stringify(name);
7
+ const pascal = (s) => s.replace(/(^|[^A-Za-z0-9])([A-Za-z0-9])/g, (_, __, c) => c.toUpperCase());
8
+ /**
9
+ * Map a `format_type` string to a TypeScript type. Anything not recognised becomes `unknown`
10
+ * rather than `string`: a type that is merely plausible is worse than one that forces a check.
11
+ */
12
+ const scalar = (pgType) => {
13
+ const base = pgType.trim().replace(/\(.*\)$/, "");
14
+ switch (base.includes(".") ? base.slice(base.lastIndexOf(".") + 1) : base) {
15
+ case "smallint":
16
+ case "integer":
17
+ case "bigint":
18
+ case "numeric":
19
+ case "decimal":
20
+ case "real":
21
+ case "double precision":
22
+ case "smallserial":
23
+ case "serial":
24
+ case "bigserial": return "number";
25
+ case "boolean": return "boolean";
26
+ case "text":
27
+ case "character varying":
28
+ case "character":
29
+ case "bpchar":
30
+ case "varchar":
31
+ case "char":
32
+ case "name":
33
+ case "citext":
34
+ case "uuid":
35
+ case "date":
36
+ case "timestamp with time zone":
37
+ case "timestamp without time zone":
38
+ case "time with time zone":
39
+ case "time without time zone":
40
+ case "interval":
41
+ case "bytea":
42
+ case "money":
43
+ case "xml":
44
+ case "inet":
45
+ case "cidr":
46
+ case "macaddr":
47
+ case "macaddr8":
48
+ case "tsvector": return "string";
49
+ default: return "unknown";
50
+ }
51
+ };
52
+ /** The type of one column's value, enum labels and array dimensions included. */
53
+ const columnType = (col) => {
54
+ let t = col.type.trim();
55
+ let dims = 0;
56
+ while (t.endsWith("[]")) {
57
+ t = t.slice(0, -2).trim();
58
+ dims++;
59
+ }
60
+ const labels = col.enumLabels;
61
+ const inner = labels?.length ? labels.map((l) => JSON.stringify(l)).join(" | ") : scalar(t);
62
+ return (dims > 0 && inner.includes(" | ") ? `(${inner})` : inner) + "[]".repeat(dims);
63
+ };
64
+ const nullableFor = (cache, col, views) => {
65
+ if (!col.nullable) return false;
66
+ if (views === "nullable" || !col.source) return true;
67
+ const base = cache.relations.get(relKey(col.source.schema, col.source.table))?.columns.find((c) => c.name === col.source.column);
68
+ return base ? base.nullable : true;
69
+ };
70
+ const writer = () => {
71
+ const lines = [];
72
+ return {
73
+ lines,
74
+ push: (depth, text) => lines.push(`${" ".repeat(depth)}${text}`)
75
+ };
76
+ };
77
+ /** `row`, `insert` and `update` of one relation. */
78
+ const shapes = (rel, sourceNullable, push, depth) => {
79
+ const cols = [...rel.columns].sort((a, b) => a.position - b.position);
80
+ const value = (c) => {
81
+ const t = columnType(c);
82
+ return t === "unknown" || !sourceNullable(c) ? t : `${t} | null`;
83
+ };
84
+ push(depth, "row: {");
85
+ for (const c of cols) push(depth + 1, `${key(c.name)}: ${value(c)};`);
86
+ push(depth, "};");
87
+ const writable = cols.filter((c) => !c.generated && c.identity !== "always");
88
+ push(depth, rel.insertable ? "insert: {" : "insert: never;");
89
+ if (rel.insertable) {
90
+ for (const c of writable) {
91
+ const optional = c.hasDefault || sourceNullable(c) ? "?" : "";
92
+ push(depth + 1, `${key(c.name)}${optional}: ${value(c)};`);
93
+ }
94
+ push(depth, "};");
95
+ }
96
+ push(depth, rel.updatable ? "update: {" : "update: never;");
97
+ if (rel.updatable) {
98
+ for (const c of writable) push(depth + 1, `${key(c.name)}?: ${value(c)};`);
99
+ push(depth, "};");
100
+ }
101
+ };
102
+ /**
103
+ * Every name that can be embedded from `rel`, resolved by the same {@link findRelationships} the
104
+ * request path uses -- so a `select` the types accept is one the library can resolve.
105
+ */
106
+ const embeds = (cache, rel) => {
107
+ const candidates = /* @__PURE__ */ new Set();
108
+ for (const r of cache.relations.values()) if (r.schema === rel.schema && r.name !== rel.name) candidates.add(r.name);
109
+ for (const fk of cache.foreignKeys) if (fk.schema === rel.schema && fk.table === rel.name && fk.cols.length === 1) candidates.add(fk.cols[0]);
110
+ for (const [k, defs] of cache.functions) {
111
+ const [schema, ...rest] = k.split(".");
112
+ if (schema !== rel.schema) continue;
113
+ if (defs.some((d) => d.argRelation?.schema === rel.schema && d.argRelation.name === rel.name && d.returnRelation)) candidates.add(rest.join("."));
114
+ }
115
+ const out = [];
116
+ for (const target of [...candidates].sort()) {
117
+ const usable = findRelationships(cache, rel, target).filter((r) => r.to.schema === rel.schema);
118
+ if (usable.length === 0) continue;
119
+ const first = usable[0];
120
+ const to = first.kind === "o2m" || first.kind === "m2m" ? "many" : "one";
121
+ const hints = usable.map((r) => JSON.stringify(r.fk.name)).join(", ");
122
+ const ambiguous = usable.length > 1 ? `; ambiguous: true; hints: [${hints}]` : "";
123
+ out.push([target, `{ rel: ${JSON.stringify(first.to.name)}; to: ${JSON.stringify(to)}${ambiguous} }`]);
124
+ }
125
+ return out;
126
+ };
127
+ /** The arguments and result of one overload. */
128
+ const signature = (fn, relationsType, cache) => {
129
+ const raw = fn.args.length === 1 && fn.args[0].name === "";
130
+ const args = raw ? columnType({
131
+ type: fn.args[0].type,
132
+ enumLabels: fn.args[0].enumLabels
133
+ }) : fn.args.length === 0 ? "{}" : `{ ${fn.args.map((a) => {
134
+ const t = columnType({
135
+ type: a.type,
136
+ enumLabels: a.enumLabels
137
+ });
138
+ return `${key(a.name)}${a.hasDefault ? "?" : ""}: ${t}`;
139
+ }).join("; ")} }`;
140
+ const target = fn.returnRelation ? cache.relations.get(relKey(fn.returnRelation.schema, fn.returnRelation.name)) : void 0;
141
+ return `{ args: ${args}; returns: ${fn.returnsVoid ? "null" : target ? `${relationsType}[${JSON.stringify(target.name)}]["row"]` : fn.returnsComposite ? "Record<string, unknown>" : columnType({
142
+ type: fn.returnType,
143
+ enumLabels: fn.returnEnumLabels
144
+ })}; set: ${fn.returnsSet}${raw ? "; raw: true" : ""} }`;
145
+ };
146
+ /** Generate the module text. Deterministic: same catalog, same bytes. */
147
+ const generateTypes = (cache, options = {}) => {
148
+ const name = options.name ?? "Database";
149
+ const views = options.views ?? "nullable";
150
+ const { lines, push } = writer();
151
+ if (options.header !== false) {
152
+ push(0, `// ${options.header ?? `Generated from the database catalog by @adrdp/pg-rest. Do not edit.`}`);
153
+ push(0, "");
154
+ }
155
+ const schemas = [...cache.schemas];
156
+ for (const schema of schemas) {
157
+ const relationsType = `${name}${pascal(schema)}Relations`;
158
+ const relations = [...cache.relations.values()].filter((r) => r.schema === schema).sort((a, b) => a.name.localeCompare(b.name));
159
+ push(0, `type ${relationsType} = {`);
160
+ for (const rel of relations) {
161
+ const sourceNullable = (c) => nullableFor(cache, c, views);
162
+ push(1, `${key(rel.name)}: {`);
163
+ shapes(rel, sourceNullable, push, 2);
164
+ push(2, `pk: [${rel.pk.map((c) => JSON.stringify(c)).join(", ")}];`);
165
+ push(2, `insertable: ${rel.insertable};`);
166
+ push(2, `updatable: ${rel.updatable};`);
167
+ push(2, `deletable: ${rel.deletable};`);
168
+ const rels = embeds(cache, rel);
169
+ if (rels.length === 0) push(2, "rel: {};");
170
+ else {
171
+ push(2, "rel: {");
172
+ for (const [target, entry] of rels) push(3, `${key(target)}: ${entry};`);
173
+ push(2, "};");
174
+ }
175
+ push(1, "};");
176
+ }
177
+ push(0, "};");
178
+ push(0, "");
179
+ const functionsType = `${name}${pascal(schema)}Functions`;
180
+ const fns = [...cache.functions.entries()].filter(([k]) => k.startsWith(`${schema}.`)).map(([k, defs]) => [k.slice(schema.length + 1), defs.filter((d) => !(d.argRelation && d.returnRelation))]).filter(([, defs]) => defs.length > 0).sort(([a], [b]) => a.localeCompare(b));
181
+ if (fns.length === 0) push(0, `type ${functionsType} = {};`);
182
+ else {
183
+ push(0, `type ${functionsType} = {`);
184
+ for (const [fnName, defs] of fns) {
185
+ const overloads = defs.map((d) => signature(d, relationsType, cache)).join(", ");
186
+ push(1, `${key(fnName)}: [${overloads}];`);
187
+ }
188
+ push(0, "};");
189
+ }
190
+ push(0, "");
191
+ }
192
+ push(0, `export type ${name} = {`);
193
+ push(1, `default: ${JSON.stringify(schemas[0] ?? "public")};`);
194
+ push(1, "schemas: {");
195
+ for (const schema of schemas) push(2, `${key(schema)}: { relations: ${name}${pascal(schema)}Relations; functions: ${name}${pascal(schema)}Functions };`);
196
+ push(1, "};");
197
+ push(0, "};");
198
+ return `${lines.join("\n")}\n`;
199
+ };
200
+
201
+ //#endregion
202
+ export { columnType, generateTypes };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adrdp/pg-rest",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "A PostgREST-compatible REST layer for PostgreSQL, consumed as a library (Bun, Node.js and other runtimes with web-standard Request/Response). You decide who the user is (RLS) — no JWT required.",
5
5
  "license": "MIT",
6
6
  "author": "Alexandre Dos Reis",
@@ -20,6 +20,10 @@
20
20
  "types": "./dist/index.d.ts",
21
21
  "default": "./dist/index.js"
22
22
  },
23
+ "./types": {
24
+ "types": "./dist/types.d.ts",
25
+ "default": "./dist/types.js"
26
+ },
23
27
  "./adapters/bun": {
24
28
  "types": "./dist/adapters/bun.d.ts",
25
29
  "default": "./dist/adapters/bun.js"
@@ -60,6 +64,8 @@
60
64
  "@changesets/cli": "^3.0.3",
61
65
  "@electric-sql/pglite": "^0.5.15",
62
66
  "@hono/node-server": "^2.1.6",
67
+ "@neondatabase/postgrest-js": "^0.2.0-beta",
68
+ "@supabase/postgrest-js": "^2.119.0",
63
69
  "@types/bun": "latest",
64
70
  "@types/pg": "^8.23.1",
65
71
  "autocannon": "^8.0.0",