true-pg 0.6.0 → 0.7.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.
@@ -1,6 +1,6 @@
1
1
  import type { DbAdapter } from "../adapter.ts";
2
2
  import type { PgType } from "../pgtype.ts";
3
- import { Canonical } from "../canonicalise.ts";
3
+ import type { Canonical } from "../canonicalise.ts";
4
4
  declare const parameterModeMap: {
5
5
  readonly i: "IN";
6
6
  readonly o: "OUT";
@@ -1,4 +1,3 @@
1
- import { canonicalise, Canonical } from "../canonicalise.js";
2
1
  import { parsePostgresTableDefinition } from "./util/parseInlineTable.js";
3
2
  const parameterModeMap = {
4
3
  i: "IN",
@@ -75,13 +74,13 @@ async function extractFunction(db, pgType) {
75
74
  if (row.arg_names && !row.arg_modes)
76
75
  row.arg_modes = row.arg_names.map(() => "i");
77
76
  const argModes = row.arg_modes?.map(mode => parameterModeMap[mode]) ?? [];
78
- const canonical_arg_types = row.arg_types ? await canonicalise(db, row.arg_types) : [];
77
+ const canonical_arg_types = row.arg_types ? await db.canonicalise(row.arg_types) : [];
79
78
  let returnType;
80
79
  const tableMatch = row.declared_return_type.match(/^TABLE\((.*)\)$/i);
81
80
  if (tableMatch) {
82
81
  const columnDefs = parsePostgresTableDefinition(row.declared_return_type);
83
82
  const columnTypes = columnDefs.map(col => col.type);
84
- const canonicalColumnTypes = await canonicalise(db, columnTypes);
83
+ const canonicalColumnTypes = await db.canonicalise(columnTypes);
85
84
  returnType = {
86
85
  kind: FunctionReturnTypeKind.InlineTable,
87
86
  columns: columnDefs.map((col, i) => ({
@@ -111,7 +110,7 @@ async function extractFunction(db, pgType) {
111
110
  else if (
112
111
  // "c" = composite type
113
112
  row.return_type_relation_kind === "c") {
114
- const canonicalReturnType = (await canonicalise(db, [row.return_type_string]))[0];
113
+ const canonicalReturnType = (await db.canonicalise([row.return_type_string]))[0];
115
114
  returnType = {
116
115
  kind: FunctionReturnTypeKind.Regular,
117
116
  type: canonicalReturnType,
@@ -120,7 +119,7 @@ async function extractFunction(db, pgType) {
120
119
  }
121
120
  else {
122
121
  console.warn(`Composite return type '${row.return_type_string}' has unexpected relkind '${row.return_type_relation_kind}' for function ${pgType.schemaName}.${row.name}`);
123
- const canonicalReturnType = (await canonicalise(db, [row.return_type_string]))[0];
122
+ const canonicalReturnType = (await db.canonicalise([row.return_type_string]))[0];
124
123
  returnType = {
125
124
  kind: FunctionReturnTypeKind.Regular,
126
125
  type: canonicalReturnType,
@@ -129,7 +128,7 @@ async function extractFunction(db, pgType) {
129
128
  }
130
129
  }
131
130
  else {
132
- const canonicalReturnType = (await canonicalise(db, [row.return_type_string]))[0];
131
+ const canonicalReturnType = (await db.canonicalise([row.return_type_string]))[0];
133
132
  returnType = {
134
133
  kind: FunctionReturnTypeKind.Regular,
135
134
  type: canonicalReturnType,
@@ -1,6 +1,6 @@
1
1
  import type { DbAdapter } from "../adapter.ts";
2
2
  import type { PgType } from "../pgtype.ts";
3
- import { Canonical } from "../canonicalise.ts";
3
+ import type { Canonical } from "../canonicalise.ts";
4
4
  export interface MaterializedViewColumn {
5
5
  name: string;
6
6
  type: Canonical;
@@ -1,4 +1,3 @@
1
- import { Canonical, canonicalise } from "../canonicalise.js";
2
1
  const extractMaterializedView = async (db, mview) => {
3
2
  // 1. Query for columns (using pg_attribute for potentially more accurate nullability)
4
3
  const columnQuery = await db.query(`
@@ -26,7 +25,7 @@ const extractMaterializedView = async (db, mview) => {
26
25
  `, [mview.name, mview.schemaName]);
27
26
  // 2. Get canonical types
28
27
  const definedTypes = columnQuery.map(row => row.definedType);
29
- const canonicalTypes = await canonicalise(db, definedTypes);
28
+ const canonicalTypes = await db.canonicalise(definedTypes);
30
29
  const columns = columnQuery.map((row, index) => ({
31
30
  name: row.name,
32
31
  type: canonicalTypes[index],
@@ -1,6 +1,6 @@
1
1
  import type { DbAdapter } from "../adapter.ts";
2
2
  import type { PgType } from "../pgtype.ts";
3
- import { Canonical } from "../canonicalise.ts";
3
+ import type { Canonical } from "../canonicalise.ts";
4
4
  /**
5
5
  * Range type in a schema with details.
6
6
  */
@@ -1,9 +1,8 @@
1
- import { Canonical, canonicalise } from "../canonicalise.js";
2
1
  const extractRange = async (db, range) => {
3
2
  // Form the fully qualified type name
4
3
  const fullTypeName = `"${range.schemaName}"."${range.name}"`;
5
4
  // Get canonical type information with all the metadata
6
- const [canonical] = await canonicalise(db, [fullTypeName]);
5
+ const [canonical] = await db.canonicalise([fullTypeName]);
7
6
  // Return the composite type with its canonical representation
8
7
  return {
9
8
  ...range,
@@ -1,6 +1,6 @@
1
1
  import { DbAdapter } from "../adapter.ts";
2
2
  import type { PgType } from "../pgtype.ts";
3
- import { Canonical } from "../canonicalise.ts";
3
+ import type { Canonical } from "../canonicalise.ts";
4
4
  export declare const updateActionMap: {
5
5
  readonly a: "NO ACTION";
6
6
  readonly r: "RESTRICT";
@@ -1,7 +1,6 @@
1
1
  import { DbAdapter } from "../adapter.js";
2
2
  import commentMapQueryPart from "./parts/commentMapQueryPart.js";
3
3
  import indexMapQueryPart from "./parts/indexMapQueryPart.js";
4
- import { Canonical, canonicalise } from "../canonicalise.js";
5
4
  export const updateActionMap = {
6
5
  a: "NO ACTION",
7
6
  r: "RESTRICT",
@@ -109,7 +108,7 @@ const extractTable = async (db, table) => {
109
108
  // Get the expanded type names from the query result
110
109
  const definedTypes = columnsQuery.map(row => row.definedType);
111
110
  // Use canonicaliseTypes to get detailed type information
112
- const canonicalTypes = await canonicalise(db, definedTypes);
111
+ const canonicalTypes = await db.canonicalise(definedTypes);
113
112
  // Combine the column information with the canonical type information
114
113
  const columns = columnsQuery.map((row, index) => ({
115
114
  name: row.name,
@@ -1,6 +1,6 @@
1
1
  import type { DbAdapter } from "../adapter.ts";
2
2
  import type { PgType } from "../pgtype.ts";
3
- import { Canonical } from "../canonicalise.ts";
3
+ import type { Canonical } from "../canonicalise.ts";
4
4
  export interface ViewColumn {
5
5
  name: string;
6
6
  type: Canonical;
@@ -1,4 +1,3 @@
1
- import { Canonical, canonicalise } from "../canonicalise.js";
2
1
  const extractView = async (db, view) => {
3
2
  // 1. Query for columns (information_schema.columns + pg_attribute)
4
3
  const columnQuery = await db.query(`
@@ -25,7 +24,7 @@ const extractView = async (db, view) => {
25
24
  `, [view.name, view.schemaName]);
26
25
  // 2. Get canonical types
27
26
  const definedTypes = columnQuery.map(row => row.definedType);
28
- const canonicalTypes = await canonicalise(db, definedTypes);
27
+ const canonicalTypes = await db.canonicalise(definedTypes);
29
28
  const columns = columnQuery.map((row, index) => ({
30
29
  name: row.name,
31
30
  type: canonicalTypes[index],
@@ -16,7 +16,7 @@ export declare const routineKindMap: {
16
16
  readonly f: "function";
17
17
  };
18
18
  export type RoutineKind = (typeof routineKindMap)[keyof typeof routineKindMap];
19
- export declare const pgTypeKinds: ("function" | "enum" | "domain" | "range" | "table" | "view" | "materializedView" | "composite")[];
19
+ export declare const pgTypeKinds: ("function" | "composite" | "domain" | "enum" | "range" | "table" | "view" | "materializedView")[];
20
20
  export type Kind = (typeof pgTypeKinds)[number];
21
21
  /**
22
22
  * Base type for Postgres objects.
package/lib/imports.js CHANGED
@@ -1,4 +1,4 @@
1
- import { dirname, relative } from "node:path";
1
+ import { dirname, relative } from "node:path/posix";
2
2
  import { eq } from "./util.js";
3
3
  export class Import {
4
4
  from;
@@ -18,8 +18,11 @@ export class Import {
18
18
  return new Import({
19
19
  from: files => {
20
20
  const schema = files.children[t.schema];
21
- const kind = schema.children[t.kind];
22
- const type = kind.children[t.name];
21
+ const kind = schema?.children[t.kind];
22
+ const type = kind?.children[t.name];
23
+ if (!schema || !kind || !type) {
24
+ throw new Error(`Type ${t.kind}/${t.name} not found in schema "${t.schema}"`);
25
+ }
23
26
  const path = `${files.name}/${schema.name}/${kind.kind}s/${type.name}.ts`;
24
27
  return relative(dirname(opts.source), path);
25
28
  },
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,188 @@
1
+ import { Canonical } from "./extractor/canonicalise.js";
2
+ import { Import, ImportList } from "./imports.js";
3
+ import { describe, it, expect } from "bun:test";
4
+ const files = {
5
+ name: "root",
6
+ type: "root",
7
+ children: {
8
+ public: {
9
+ name: "public",
10
+ type: "schema",
11
+ children: {
12
+ function: {
13
+ kind: "function",
14
+ type: "kind",
15
+ children: {},
16
+ },
17
+ view: {
18
+ kind: "view",
19
+ type: "kind",
20
+ children: {},
21
+ },
22
+ materializedView: {
23
+ kind: "materializedView",
24
+ type: "kind",
25
+ children: {},
26
+ },
27
+ composite: {
28
+ kind: "composite",
29
+ type: "kind",
30
+ children: {},
31
+ },
32
+ table: {
33
+ kind: "table",
34
+ type: "kind",
35
+ children: {
36
+ testTable: {
37
+ name: "testTable",
38
+ type: "type",
39
+ },
40
+ },
41
+ },
42
+ range: {
43
+ kind: "range",
44
+ type: "kind",
45
+ children: {},
46
+ },
47
+ enum: {
48
+ kind: "enum",
49
+ type: "kind",
50
+ children: {
51
+ testEnum: {
52
+ name: "testEnum",
53
+ type: "type",
54
+ },
55
+ },
56
+ },
57
+ domain: {
58
+ kind: "domain",
59
+ type: "kind",
60
+ children: {},
61
+ },
62
+ },
63
+ },
64
+ },
65
+ };
66
+ describe("Import Class", () => {
67
+ it("should initialize correctly with constructor arguments", () => {
68
+ const importInstance = new Import({
69
+ from: "module/path",
70
+ namedImports: ["component"],
71
+ star: "starImport",
72
+ default: "defaultImport",
73
+ typeOnly: true,
74
+ });
75
+ expect(importInstance.from).toBe("module/path");
76
+ expect(importInstance.namedImports).toEqual(["component"]);
77
+ expect(importInstance.star).toBe("starImport");
78
+ expect(importInstance.default).toBe("defaultImport");
79
+ expect(importInstance.typeOnly).toBe(true);
80
+ });
81
+ it("should create import fromInternal method correctly", () => {
82
+ const importInstance = Import.fromInternal({
83
+ source: "root/public/testEnum/testEnum.ts",
84
+ type: {
85
+ schema: "public",
86
+ kind: Canonical.Kind.Enum,
87
+ name: "testEnum",
88
+ canonical_name: "test",
89
+ dimensions: 1,
90
+ enum_values: ["test1", "test2", "test3"],
91
+ original_type: "string",
92
+ },
93
+ });
94
+ expect(importInstance).toBeInstanceOf(Import);
95
+ expect(typeof importInstance.from === "function" ||
96
+ typeof importInstance.from === "string").toBe(true);
97
+ if (typeof importInstance.from === "function") {
98
+ const generatedPath = importInstance.from(files);
99
+ expect(generatedPath).toBe("../enums/testEnum.ts");
100
+ }
101
+ });
102
+ it("should handle missing properties in fromInternal method", () => {
103
+ const importInstance = Import.fromInternal({
104
+ source: "root/public/testEnum/testEnum.ts",
105
+ type: {
106
+ schema: "public",
107
+ kind: Canonical.Kind.Enum,
108
+ name: "testEnum",
109
+ canonical_name: "test",
110
+ dimensions: 1,
111
+ enum_values: ["test1", "test2", "test3"],
112
+ original_type: "string",
113
+ },
114
+ });
115
+ expect(importInstance).toBeInstanceOf(Import);
116
+ expect(typeof importInstance.from === "function" ||
117
+ typeof importInstance.from === "string").toBe(true);
118
+ if (typeof importInstance.from === "function") {
119
+ const generatedPath = importInstance.from(files);
120
+ expect(generatedPath).toBe("../enums/testEnum.ts");
121
+ }
122
+ });
123
+ it("should handle missing `namedImports` or other constructor properties", () => {
124
+ const importInstance = new Import({ from: "module/path" });
125
+ expect(importInstance.from).toBe("module/path");
126
+ expect(importInstance.namedImports).toBeUndefined();
127
+ expect(importInstance.star).toBeUndefined();
128
+ expect(importInstance.default).toBeUndefined();
129
+ expect(importInstance.typeOnly).toBe(false);
130
+ });
131
+ });
132
+ describe("ImportList Class", () => {
133
+ it("should add imports correctly", () => {
134
+ const importList = new ImportList();
135
+ const newImport = new Import({
136
+ from: "module/path",
137
+ namedImports: ["MyComponent"],
138
+ });
139
+ importList.add(newImport);
140
+ expect(importList.imports).toHaveLength(1);
141
+ expect(importList.imports[0]).toBe(newImport);
142
+ });
143
+ it("should merge import lists correctly", () => {
144
+ const list1 = new ImportList([
145
+ new Import({ from: "module1", namedImports: ["a"] }),
146
+ ]);
147
+ const list2 = new ImportList([
148
+ new Import({ from: "module2", namedImports: ["b"] }),
149
+ ]);
150
+ const mergedList = ImportList.merge([list1, list2]);
151
+ expect(mergedList.imports).toHaveLength(2);
152
+ });
153
+ it("should merge import lists correctly with empty list", () => {
154
+ const list1 = new ImportList([
155
+ new Import({ from: "module1", namedImports: ["a"] }),
156
+ ]);
157
+ const list2 = new ImportList([]);
158
+ const mergedList = ImportList.merge([list1, list2]);
159
+ expect(mergedList.imports).toHaveLength(1);
160
+ });
161
+ it("should stringify imports correctly", () => {
162
+ const importList = new ImportList([
163
+ new Import({ from: "module1", namedImports: ["a"] }),
164
+ new Import({ from: "module1", namedImports: ["b"] }),
165
+ ]);
166
+ const result = importList.stringify(files);
167
+ expect(result).toContain('import { a, b } from "module1";');
168
+ });
169
+ it("should handle empty ImportList gracefully", () => {
170
+ const importList = new ImportList();
171
+ const files = {
172
+ name: "root",
173
+ type: "root",
174
+ children: {},
175
+ };
176
+ const result = importList.stringify(files);
177
+ expect(result).toBe("");
178
+ });
179
+ it("should handle duplicate imports and avoid repetition", () => {
180
+ const importList = new ImportList([
181
+ new Import({ from: "module1", namedImports: ["a"] }),
182
+ new Import({ from: "module1", namedImports: ["a"] }),
183
+ ]);
184
+ const result = importList.stringify(files);
185
+ expect(result).toBe('import { a } from "module1";');
186
+ expect(result.split("\n")).toBeArrayOfSize(1);
187
+ });
188
+ });
package/lib/index.js CHANGED
@@ -14,11 +14,27 @@ const yellow = (str) => (NO_COLOR ? str : `\x1b[33m${str}\x1b[0m`);
14
14
  const blue = (str) => (NO_COLOR ? str : `\x1b[34m${str}\x1b[0m`);
15
15
  const bold = (str) => (NO_COLOR ? str : `\x1b[1m${str}\x1b[0m`);
16
16
  const underline = (str) => (NO_COLOR ? str : `\x1b[4m${str}\x1b[0m`);
17
+ const formatTime = (time) => {
18
+ const mins = Math.floor(time / 60000);
19
+ const secs = Math.floor((time % 60000) / 1000);
20
+ const ms = Math.floor(time % 1000);
21
+ const us = Math.floor((time * 1000) % 1000)
22
+ .toString()
23
+ .padStart(3, "0");
24
+ const parts = [];
25
+ if (mins)
26
+ parts.push(mins + "m");
27
+ if (secs)
28
+ parts.push(secs + "s");
29
+ if (!mins)
30
+ parts.push(ms + (!secs && us ? "." + us : "") + "ms");
31
+ return parts.join("");
32
+ };
17
33
  const THRESHOLD1 = 800;
18
34
  const THRESHOLD2 = 1500;
19
35
  const time = (start, addParens = true) => {
20
36
  const diff = performance.now() - start;
21
- const diffstr = diff.toFixed(2) + "ms";
37
+ const diffstr = formatTime(diff);
22
38
  const str = addParens ? parens(diffstr) : diffstr;
23
39
  if (diff < THRESHOLD1)
24
40
  return green(str);
@@ -31,6 +31,8 @@ export const Kysely = createGenerator(opts => {
31
31
  qualified = `Generated<${qualified}>`;
32
32
  ky(ctx, "Generated");
33
33
  }
34
+ // TODO: Use ColumnType for appropriate cases:
35
+ // composite, jsonb, json, date types, etc
34
36
  let out = col.comment ? `/** ${col.comment} */\n\t` : "";
35
37
  out += quoteI(col.name);
36
38
  out += `: ${qualified}`;
package/lib/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Canonical, type TableDetails, type ViewDetails, type MaterializedViewDetails, type EnumDetails, type CompositeTypeDetails, type DomainDetails, type RangeDetails, type FunctionDetails, type SchemaType, type Schema, type FunctionReturnType } from "./extractor/index.ts";
2
2
  import type { ImportList } from "./imports.ts";
3
- export declare const allowed_kind_names: ("function" | "enum" | "domain" | "range" | "table" | "view" | "materializedView" | "composite")[];
3
+ export declare const allowed_kind_names: ("function" | "composite" | "domain" | "enum" | "range" | "table" | "view" | "materializedView")[];
4
4
  export type allowed_kind_names = (typeof allowed_kind_names)[number];
5
5
  export interface FolderStructure {
6
6
  name: string;
package/lib/util.d.ts CHANGED
@@ -1,3 +1,10 @@
1
+ export declare const unreachable: (value: never) => never;
2
+ export declare class Deferred<T> {
3
+ resolve: (value: T) => void;
4
+ reject: (reason?: any) => void;
5
+ promise: Promise<T>;
6
+ constructor();
7
+ }
1
8
  export declare const eq: <T>(a: T, b: T) => boolean;
2
9
  export declare const toPascalCase: (str: string) => string;
3
10
  export declare const to_snake_case: (str: string) => string;
package/lib/util.js CHANGED
@@ -1,3 +1,17 @@
1
+ export const unreachable = (value) => {
2
+ throw new Error(`Fatal: Reached unreachable code: ${value}`);
3
+ };
4
+ export class Deferred {
5
+ resolve;
6
+ reject;
7
+ promise;
8
+ constructor() {
9
+ this.promise = new Promise((resolve, reject) => {
10
+ this.resolve = resolve;
11
+ this.reject = reject;
12
+ });
13
+ }
14
+ }
1
15
  export const eq = (a, b) => {
2
16
  if (a === b)
3
17
  return true;
package/lib/zod/index.js CHANGED
@@ -22,16 +22,12 @@ export const Zod = createGenerator(opts => {
22
22
  out += quoteI(col.name);
23
23
  const nullable = col.isNullable || col.generated === "BY DEFAULT" || col.defaultValue;
24
24
  let type = generator.formatType(ctx, col.type, { nullable });
25
- if (nullable)
26
- type += `.optional()`;
27
25
  out += `: ${type}`;
28
26
  return `\t${out},\n`;
29
27
  };
30
28
  const composite_attribute = (ctx, attr) => {
31
29
  let out = quoteI(attr.name);
32
30
  out += `: ${generator.formatType(ctx, attr.type, { nullable: attr.isNullable })}`;
33
- if (attr.isNullable)
34
- out += ".optional()";
35
31
  return out;
36
32
  };
37
33
  const generator = {
@@ -80,7 +76,7 @@ export const Zod = createGenerator(opts => {
80
76
  if ("dimensions" in type)
81
77
  base += ".array()".repeat(type.dimensions);
82
78
  if (attr?.nullable)
83
- base += ".nullable()";
79
+ base += ".optional()";
84
80
  return base;
85
81
  },
86
82
  table(ctx, table) {
@@ -163,8 +159,6 @@ export const Zod = createGenerator(opts => {
163
159
  for (const param of inputParams) {
164
160
  // TODO: update imports for non-primitive types based on typeInfo.kind
165
161
  out += "\t\t" + this.formatType(ctx, param.type, { nullable: param.hasDefault });
166
- if (param.hasDefault)
167
- out += ".optional()";
168
162
  out += `, // ${param.name}\n`;
169
163
  }
170
164
  out += "\t])";
@@ -249,9 +243,7 @@ export const Zod = createGenerator(opts => {
249
243
  fullIndex(ctx, schemas, main_generator) {
250
244
  const generator = main_generator ?? this;
251
245
  const parts = [];
252
- parts.push(schemas
253
- .map(s => `import { ${generator.formatSchemaName(s.name)} } from "./${s.name}/index.ts";`)
254
- .join("\n"));
246
+ parts.push(schemas.map(s => `import { ${generator.formatSchemaName(s.name)} } from "./${s.name}/index.ts";`).join("\n"));
255
247
  {
256
248
  let validator = `export const Validators = {\n`;
257
249
  validator += join(schemas.map(schema => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "true-pg",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "module": "lib/index.js",
6
6
  "main": "lib/index.js",