true-pg 0.3.2 → 0.4.1

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/README.md CHANGED
@@ -12,7 +12,49 @@ yarn add true-pg
12
12
  bun add true-pg
13
13
  ```
14
14
 
15
- ## Usage
15
+ ## Quickstart
16
+
17
+ ```bash
18
+ npx true-pg --all-adapters --uri postgres://user:password@localhost:5432/database --out ./models
19
+ ```
20
+
21
+ This will generate a `models` directory with the following structure:
22
+
23
+ ```
24
+ models/
25
+ ├── index.ts
26
+ ├── public/
27
+ │ ├── index.ts
28
+ │ ├── tables/
29
+ │ │ ├── index.ts
30
+ │ │ └── User.ts
31
+ │ └── views/
32
+ │ ├── enums/
33
+ │ └── ...
34
+ ```
35
+
36
+ You can then import the `Database` type from the `index.ts` file and pass it to Kysely:
37
+
38
+ ```typescript
39
+ import { Kysely } from "kysely";
40
+ import { Database } from "./models/index.ts";
41
+
42
+ const db = new Kysely<Database>( ... );
43
+ ```
44
+
45
+ ## Sample Database
46
+
47
+ A sample database and generated models are available in the `sample` directory. You can browse the generated models in the `sample/models` directory.
48
+
49
+ To run the sample yourself, run:
50
+
51
+ ```bash
52
+ bun install # install dependencies
53
+ cd sample
54
+ bun run ../src/bin.ts
55
+ ```
56
+
57
+ ## Detailed Usage
16
58
 
17
59
  ### Command Line Interface
18
60
 
@@ -33,20 +75,23 @@ You can configure true-pg either through command-line arguments or a config file
33
75
 
34
76
  ### Configuration file
35
77
 
36
- The tool looks for configuration in the following locations (in order):
78
+ If an explicit config file is not provided via `--config`, true-pg will look for a config file in the current working directory.
37
79
 
38
- 1. `.truepgrc.json`
39
- 2. `.config/.truepgrc.json`
80
+ We use cosmiconfig to load the config file. See the [cosmiconfig docs](https://github.com/cosmiconfig/cosmiconfig#usage-for-end-users) for all possible config file formats.
81
+
82
+ The recommended default is `truepg.config.ts`, or `.config/truepg.ts`.
40
83
 
41
84
  Example config file:
42
85
 
43
- ```json
44
- {
45
- "uri": "postgres://user:password@localhost:5432/database",
46
- "out": "src/models",
47
- "adapters": ["kysely", "zod"],
48
- "defaultSchema": "public"
49
- }
86
+ ```typescript
87
+ import { config } from "true-pg";
88
+
89
+ export default config({
90
+ uri: "postgres://user:password@localhost:5432/database",
91
+ out: "src/models",
92
+ adapters: ["kysely", "zod"],
93
+ defaultSchema: "public",
94
+ });
50
95
  ```
51
96
 
52
97
  ## Configuration Options
@@ -102,6 +147,7 @@ const generator = createGenerator(opts => ({
102
147
  await generate(
103
148
  {
104
149
  uri: "postgres://user:password@localhost:5432/database",
150
+ adapters: [], // empty array to disable adapters
105
151
  out: "src/models",
106
152
  },
107
153
  [generator],
package/lib/bin.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import mri from "mri";
3
- import { generate, adapters } from "./index.js";
3
+ import { generate } from "./index.js";
4
+ import { adapters } from "./config.js";
4
5
  const args = process.argv.slice(2);
5
6
  const opts = mri(args, {
6
7
  boolean: ["help", "all-adapters"],
@@ -0,0 +1,51 @@
1
+ import type { Extractor } from "./extractor/index.ts";
2
+ import { type Deunionise } from "./util.ts";
3
+ export type ExtractorConfig = Exclude<ConstructorParameters<typeof Extractor>[0], string | undefined>;
4
+ export interface BaseConfig {
5
+ /** The output directory for the generated models. Default: "models" */
6
+ out?: string;
7
+ /** Adapters to enable. Currently supported adapters are "kysely" and "zod". Default: ["kysely"] */
8
+ adapters?: ("kysely" | "zod")[];
9
+ /** The default schema to use for the generated models. These will be unprefixed in the final `Database` interface. Default: "public" */
10
+ defaultSchema?: string;
11
+ }
12
+ export interface PgConfig extends BaseConfig {
13
+ /** An instance of node-postgres Client or Pool, or an instance of Pglite. */
14
+ pg: ExtractorConfig["pg"];
15
+ }
16
+ export interface UriConfig extends BaseConfig {
17
+ /** A connection string for node-postgres Pool. */
18
+ uri: ExtractorConfig["uri"];
19
+ }
20
+ export interface ConfigConfig extends BaseConfig {
21
+ /** A configuration object for node-postgres Pool. */
22
+ config: ExtractorConfig["config"];
23
+ }
24
+ export type TruePGConfig = Deunionise<PgConfig | UriConfig | ConfigConfig>;
25
+ export declare const adapters: {
26
+ kysely: import("./types.ts").createGenerator;
27
+ zod: import("./types.ts").createGenerator;
28
+ };
29
+ export declare function config(opts: TruePGConfig): {
30
+ out: string;
31
+ adapters: ("kysely" | "zod")[];
32
+ defaultSchema: string;
33
+ pg: ExtractorConfig["pg"];
34
+ uri?: undefined;
35
+ config?: undefined;
36
+ } | {
37
+ out: string;
38
+ adapters: ("kysely" | "zod")[];
39
+ defaultSchema: string;
40
+ uri: ExtractorConfig["uri"];
41
+ pg?: undefined;
42
+ config?: undefined;
43
+ } | {
44
+ out: string;
45
+ adapters: ("kysely" | "zod")[];
46
+ defaultSchema: string;
47
+ config: ExtractorConfig["config"];
48
+ pg?: undefined;
49
+ uri?: undefined;
50
+ };
51
+ export type ValidatedConfig = ReturnType<typeof config>;
package/lib/config.js ADDED
@@ -0,0 +1,32 @@
1
+ import { normalize as normalise } from "node:path";
2
+ import {} from "./util.js";
3
+ import { Kysely } from "./kysely/index.js";
4
+ import { Zod } from "./zod/index.js";
5
+ export const adapters = {
6
+ kysely: Kysely,
7
+ zod: Zod,
8
+ };
9
+ const availableAdapters = Object.keys(adapters);
10
+ export function config(opts) {
11
+ const out = normalise(opts.out || "./models");
12
+ const adapters = opts.adapters || ["kysely"];
13
+ const defaultSchema = opts.defaultSchema || "public";
14
+ for (const adapter of opts.adapters ?? []) {
15
+ if (!availableAdapters.includes(adapter)) {
16
+ console.error('Requested adapter "%s" not found.', adapter);
17
+ console.error("Available adapters: %s", availableAdapters.join(", "));
18
+ console.error("See documentation for more information.");
19
+ process.exit(1);
20
+ }
21
+ }
22
+ if (!("uri" in opts) && !("config" in opts) && !("pg" in opts)) {
23
+ console.error("One of these options are required in your config file: uri, config, pg. See documentation for more information.");
24
+ process.exit(1);
25
+ }
26
+ return {
27
+ ...opts,
28
+ out,
29
+ adapters,
30
+ defaultSchema,
31
+ };
32
+ }
@@ -1,8 +1,9 @@
1
- import { Client as Pg } from "pg";
1
+ import * as Pg from "pg";
2
2
  import { PGlite as Pglite } from "@electric-sql/pglite";
3
3
  export declare class DbAdapter {
4
4
  private client;
5
- constructor(client: Pg | Pglite);
5
+ private external?;
6
+ constructor(client: Pg.Client | Pg.Pool | Pglite, external?: boolean | undefined);
6
7
  connect(): Promise<void>;
7
8
  /**
8
9
  * Execute a read query and return just the rows
@@ -1,12 +1,19 @@
1
- import { Client as Pg } from "pg";
1
+ import * as Pg from "pg";
2
2
  import { PGlite as Pglite } from "@electric-sql/pglite";
3
3
  export class DbAdapter {
4
4
  client;
5
- constructor(client) {
5
+ external;
6
+ constructor(client, external) {
6
7
  this.client = client;
8
+ this.external = external;
7
9
  }
8
10
  async connect() {
9
- if (this.client instanceof Pg) {
11
+ if (this.external)
12
+ return;
13
+ if (this.client instanceof Pg.Pool) {
14
+ // queries will automatically checkout a client and return it to the pool
15
+ }
16
+ else if (this.client instanceof Pg.Client) {
10
17
  return this.client.connect();
11
18
  }
12
19
  else if (this.client instanceof Pglite) {
@@ -43,7 +50,12 @@ export class DbAdapter {
43
50
  * Close the connection if needed
44
51
  */
45
52
  async close() {
46
- if (this.client instanceof Pg) {
53
+ if (this.external)
54
+ return;
55
+ if (this.client instanceof Pg.Pool) {
56
+ this.client.end();
57
+ }
58
+ else if (this.client instanceof Pg.Client) {
47
59
  await this.client.end();
48
60
  }
49
61
  else if (this.client instanceof Pglite) {
@@ -1,4 +1,4 @@
1
- import { Client as Pg, type ConnectionConfig } from "pg";
1
+ import Pg from "pg";
2
2
  import { PGlite as Pglite } from "@electric-sql/pglite";
3
3
  import { type TableDetails } from "./kinds/table.ts";
4
4
  import { type ViewDetails } from "./kinds/view.ts";
@@ -76,9 +76,9 @@ export declare class Extractor {
76
76
  * @param connectionConfig - Connection string or configuration object for Postgres connection
77
77
  */
78
78
  constructor(opts: {
79
- pg?: Pg | Pglite;
79
+ pg?: Pg.Client | Pg.Pool | Pglite;
80
80
  uri?: string;
81
- config?: ConnectionConfig;
81
+ config?: Pg.ConnectionConfig;
82
82
  });
83
83
  canonicalise(types: string[]): Promise<Canonical[]>;
84
84
  getBuiltinTypes(): Promise<{
@@ -1,4 +1,4 @@
1
- import { Client as Pg } from "pg";
1
+ import Pg from "pg";
2
2
  import { PGlite as Pglite } from "@electric-sql/pglite";
3
3
  import { DbAdapter } from "./adapter.js";
4
4
  import extractTable, {} from "./kinds/table.js";
@@ -43,14 +43,14 @@ export class Extractor {
43
43
  if (opts.pg)
44
44
  pg = opts.pg;
45
45
  else if (opts.uri)
46
- pg = new Pg({ connectionString: opts.uri });
46
+ pg = new Pg.Pool({ connectionString: opts.uri });
47
47
  else if (opts.config)
48
- pg = new Pg(opts.config);
48
+ pg = new Pg.Pool(opts.config);
49
49
  else {
50
50
  console.error("One of these options are required in your config file: pg, uri, config. See documentation for more information.");
51
51
  process.exit(1);
52
52
  }
53
- this.db = new DbAdapter(pg);
53
+ this.db = new DbAdapter(pg, opts.pg ? true : false);
54
54
  }
55
55
  async canonicalise(types) {
56
56
  return canonicalise(this.db, types);
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type TruePGConfig, type createGenerator } from "./types.ts";
2
- export { config } from "./types.ts";
3
- export declare const adapters: Record<string, createGenerator>;
1
+ import { type createGenerator } from "./types.ts";
2
+ import { type TruePGConfig, type ValidatedConfig, config } from "./config.ts";
3
+ export { type TruePGConfig, type ValidatedConfig, config };
4
4
  export declare function generate(opts: TruePGConfig, generators?: createGenerator[]): Promise<void>;
package/lib/index.js CHANGED
@@ -3,12 +3,12 @@ import { rm, mkdir, writeFile } from "node:fs/promises";
3
3
  import { Nodes, allowed_kind_names } from "./types.js";
4
4
  import { existsSync } from "node:fs";
5
5
  import { join } from "./util.js";
6
- export { config } from "./types.js";
7
- import { Kysely } from "./kysely/index.js";
8
- import { Zod } from "./zod/index.js";
9
- export const adapters = {
10
- kysely: Kysely,
11
- zod: Zod,
6
+ import { join as joinpath } from "node:path";
7
+ import { config, adapters } from "./config.js";
8
+ export { config };
9
+ const time = (start) => {
10
+ const end = performance.now();
11
+ return (end - start).toFixed(2);
12
12
  };
13
13
  const filter_function = (func) => {
14
14
  const typesToFilter = [
@@ -77,7 +77,7 @@ const multifile = async (generators, schemas, opts) => {
77
77
  };
78
78
  for (const schema of Object.values(schemas)) {
79
79
  console.log("Selected schema '%s':\n", schema.name);
80
- const schemaDir = `${out}/${schema.name}`;
80
+ const schemaDir = joinpath(out, schema.name);
81
81
  // skip functions that cannot be represented in JavaScript
82
82
  schema.functions = schema.functions.filter(filter_function);
83
83
  {
@@ -92,11 +92,11 @@ const multifile = async (generators, schemas, opts) => {
92
92
  if (schema[kind].length < 1)
93
93
  continue;
94
94
  createIndex = true;
95
- await mkdir(`${schemaDir}/${kind}`, { recursive: true });
95
+ await mkdir(joinpath(schemaDir, kind), { recursive: true });
96
96
  console.log(" Creating %s:\n", kind);
97
97
  for (const [i, item] of schema[kind].entries()) {
98
98
  const index = "[" + (i + 1 + "]").padEnd(3, " ");
99
- const filename = `${schemaDir}/${kind}/${def_gen.formatSchemaType(item)}.ts`;
99
+ const filename = joinpath(schemaDir, kind, def_gen.formatSchemaType(item) + ".ts");
100
100
  const exists = await existsSync(filename);
101
101
  if (exists) {
102
102
  warnings.push(`Skipping ${item.kind} "${item.name}": formatted name clashes. Wanted to create ${filename}`);
@@ -127,54 +127,52 @@ const multifile = async (generators, schemas, opts) => {
127
127
  parts.push(file);
128
128
  file = join(parts);
129
129
  await write(filename, file);
130
+ console.log(" %s %s \x1b[32m(%sms)\x1B[0m", index, filename, time(start));
131
+ }
132
+ {
133
+ const start = performance.now();
134
+ const kindIndex = join(gens.map(gen => gen.schemaKindIndex(schema, kind, def_gen)));
135
+ const kindIndexFilename = joinpath(schemaDir, kind, "index.ts");
136
+ await write(kindIndexFilename, kindIndex);
130
137
  const end = performance.now();
131
- console.log(" %s %s \x1b[32m(%sms)\x1B[0m", index, filename, (end - start).toFixed(2));
138
+ console.log(" ✅ Created %s index: %s \x1b[32m(%sms)\x1B[0m\n", kind, kindIndexFilename, (end - start).toFixed(2));
132
139
  }
133
- const kindIndex = join(gens.map(gen => gen.schemaKindIndex(schema, kind, def_gen)));
134
- const kindIndexFilename = `${schemaDir}/${kind}/index.ts`;
135
- await write(kindIndexFilename, kindIndex);
136
- console.log(' ✅ Created "%s" %s index: %s\n', schema.name, kind, kindIndexFilename);
137
140
  }
138
141
  if (!createIndex)
139
142
  continue;
140
- const index = join(gens.map(gen => gen.schemaIndex(schema, def_gen)));
141
- const indexFilename = `${out}/${schema.name}/index.ts`;
142
- await write(indexFilename, index);
143
- console.log(" Created schema index: %s\n", indexFilename);
143
+ {
144
+ const start = performance.now();
145
+ const index = join(gens.map(gen => gen.schemaIndex(schema, def_gen)));
146
+ const indexFilename = joinpath(schemaDir, "index.ts");
147
+ await write(indexFilename, index);
148
+ console.log(" Created schema index: %s \x1b[32m(%sms)\x1B[0m\n", indexFilename, time(start));
149
+ }
150
+ }
151
+ {
152
+ const start = performance.now();
153
+ const fullIndex = join(gens.map(gen => gen.fullIndex(Object.values(schemas))));
154
+ const fullIndexFilename = joinpath(out, "index.ts");
155
+ await write(fullIndexFilename, fullIndex);
156
+ console.log("Created full index: %s \x1b[32m(%sms)\x1B[0m", fullIndexFilename, time(start));
144
157
  }
145
- const fullIndex = join(gens.map(gen => gen.fullIndex(Object.values(schemas))));
146
- const fullIndexFilename = `${out}/index.ts`;
147
- await write(fullIndexFilename, fullIndex);
148
- console.log("Created full index: %s", fullIndexFilename);
149
158
  if (warnings.length > 0) {
150
159
  console.log("\nWarnings generated:");
151
160
  console.log(warnings.map(warning => "* " + warning).join("\n"));
152
161
  }
153
162
  };
154
163
  export async function generate(opts, generators) {
155
- const out = opts.out || "./models";
156
- if (!("uri" in opts) && !("config" in opts) && !("pg" in opts)) {
157
- console.error("One of these options are required in your config file: uri, config, pg. See documentation for more information.");
158
- process.exit(1);
159
- }
164
+ const validated = config(opts);
165
+ const out = validated.out;
160
166
  const extractor = new Extractor(opts);
161
167
  const start = performance.now();
162
168
  const schemas = await extractor.extractSchemas();
163
169
  const end = performance.now();
164
- console.log("Extracted schemas in \x1b[32m%sms\x1b[0m", (end - start).toFixed(2));
165
- console.info("Adapters enabled: %s\n", opts.adapters.join(", "));
166
- generators ??= opts.adapters.map(adapter => {
167
- const selected = adapters[adapter];
168
- if (!selected)
169
- throw new Error(`Requested adapter ${adapter} not found`);
170
- return selected;
171
- });
172
- console.log("Clearing directory and generating schemas at '%s'", out);
170
+ console.log("Extracted schemas \x1b[32m(%sms)\x1b[0m\n", (end - start).toFixed(2));
171
+ console.info("Adapters enabled: %s\n", validated.adapters.join(", "));
172
+ generators = validated.adapters.map(adapter => adapters[adapter]).concat(generators ?? []);
173
+ console.log("Clearing directory and generating schemas at '%s'\n", out);
173
174
  await rm(out, { recursive: true, force: true });
174
175
  await mkdir(out, { recursive: true });
175
- await multifile(generators, schemas, { ...opts, out });
176
- {
177
- const end = performance.now();
178
- console.log("Completed in \x1b[32m%sms\x1b[0m", (end - start).toFixed(2));
179
- }
176
+ await multifile(generators, schemas, validated);
177
+ console.log("Completed in \x1b[32m%sms\x1b[0m", time(start));
180
178
  }
package/lib/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Canonical, type Extractor, 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";
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
  export declare const allowed_kind_names: readonly ["tables", "views", "materializedViews", "enums", "composites", "functions", "domains", "ranges"];
3
3
  export type allowed_kind_names = (typeof allowed_kind_names)[number];
4
4
  export interface FolderStructure {
@@ -64,23 +64,6 @@ export declare namespace Nodes {
64
64
  star: boolean;
65
65
  }
66
66
  }
67
- export type ExtractorConfig = Exclude<ConstructorParameters<typeof Extractor>[0], string | undefined>;
68
- export interface BaseConfig {
69
- out: string;
70
- adapters: string[];
71
- defaultSchema?: string;
72
- }
73
- export interface PgConfig extends BaseConfig {
74
- pg: ExtractorConfig["pg"];
75
- }
76
- export interface UriConfig extends BaseConfig {
77
- uri: ExtractorConfig["uri"];
78
- }
79
- export interface ConfigConfig extends BaseConfig {
80
- config: ExtractorConfig["config"];
81
- }
82
- export type TruePGConfig = PgConfig | UriConfig | ConfigConfig;
83
- export declare function config(opts: TruePGConfig): TruePGConfig;
84
67
  export interface CreateGeneratorOpts {
85
68
  defaultSchema?: string;
86
69
  warnings: string[];
package/lib/types.js CHANGED
@@ -175,8 +175,5 @@ export var Nodes;
175
175
  }
176
176
  Nodes.ImportList = ImportList;
177
177
  })(Nodes || (Nodes = {}));
178
- export function config(opts) {
179
- return opts;
180
- }
181
178
  /* convenience function to create a generator with type inference */
182
179
  export const createGenerator = (generatorCreator) => generatorCreator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "true-pg",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "module": "lib/index.js",
6
6
  "main": "lib/index.js",