diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d1bcf1d..8c356d6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # main +- Add automatic parsing of PostgreSQL check constraints to generate ReScript polyvariant types for enumeration-style constraints. Supports both `column IN (value1, value2, ...)` and `column = ANY (ARRAY[value1, value2, ...])` patterns with string and integer values. +- Remove dependency on `@rescript/core` since it's not really used. + # 2.6.0 - Improve `pg` bindings. diff --git a/RESCRIPT.md b/RESCRIPT.md index 657cb280..ce3164a1 100644 --- a/RESCRIPT.md +++ b/RESCRIPT.md @@ -107,6 +107,69 @@ In order for this mode to work, you need one more thing - configure the `rescrip With that, you should be able to write queries directly in your ReScript source, and with the `watch` mode enabled have a seamless experience with types autogenerated and wired up for you. +## Check Constraint Support + +`pgtyped-rescript` automatically analyzes PostgreSQL check constraints and generates ReScript polyvariant types for enumeration-style constraints. This provides compile-time safety for constrained database fields. + +### Supported Constraint Patterns + +The following constraint patterns are automatically detected and converted to polyvariant types: + +```sql +-- Pattern 1: IN clause +status TEXT CHECK (status IN ('published', 'draft', 'archived')), + +-- Pattern 2: ANY with ARRAY +format TEXT CHECK (format = ANY (ARRAY['hardcover'::text, 'paperback'::text, 'ebook'::text])), + +-- Both string and integer values are supported +priority INTEGER CHECK (priority IN (1, 2, 3, 4, 5)) +``` + +### Generated Types + +For a table with these constraints: + +```sql +CREATE TABLE books ( + id SERIAL PRIMARY KEY, + status TEXT CHECK (status IN ('published', 'draft', 'archived')), + priority INTEGER CHECK (priority IN (1, 2, 3, 4, 5)) +); +``` + +The generated ReScript types will include: + +```rescript +type result = { + id: int, + status: option<[#"published" | #"draft" | #"archived"]>, + priority: option<[#1 | #2 | #3 | #4 | #5]>, +} +``` + +### Limitations + +- **Float constraints**: Not supported since ReScript polyvariants cannot represent float literals +- **Complex constraints**: Only simple enumeration patterns are supported (no `BETWEEN`, `OR` logic, function calls, etc.) +- **Mixed types**: Constraints mixing different data types in the same clause are not supported + +### Example + +```sql +/* @name getBooksByStatus */ +SELECT * FROM books WHERE status = :status; +``` + +```rescript +// The generated function will accept a polyvariant for status +let books = await client->GetBooksByStatus.many({ + status: #published // Compile-time checked against the database constraint! +}) +``` + +This feature works seamlessly with both separate SQL files and SQL-in-ReScript modes. + ## API ### `PgTyped` diff --git a/package-lock.json b/package-lock.json index ebf2dc08..db58b09a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4228,6 +4228,11 @@ "node": ">=8" } }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==" + }, "node_modules/dotenv": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", @@ -7820,6 +7825,11 @@ "node": ">= 0.6.0" } }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -7893,6 +7903,32 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, + "node_modules/nearley/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -9573,6 +9609,15 @@ "node": ">= 10.x" } }, + "node_modules/pgsql-ast-parser": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/pgsql-ast-parser/-/pgsql-ast-parser-12.0.1.tgz", + "integrity": "sha512-pe8C6Zh5MsS+o38WlSu18NhrTjAv1UNMeDTs2/Km2ZReZdYBYtwtbWGZKK2BM2izv5CrQpbmP0oI10wvHOwv4A==", + "dependencies": { + "moo": "^0.5.1", + "nearley": "^2.19.5" + } + }, "node_modules/pgtyped-rescript": { "resolved": "packages/cli", "link": true @@ -9844,6 +9889,23 @@ "node": ">=8" } }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==" + }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -10317,6 +10379,14 @@ "node": ">=8" } }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "engines": { + "node": ">=0.12" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -12320,7 +12390,6 @@ "dependencies": { "@pgtyped/parser": "^2.1.0", "@pgtyped/wire": "^2.2.0", - "@rescript/core": "1.6.0", "@rescript/tools": "0.6.4", "camel-case": "^4.1.1", "chalk": "^4.0.0", @@ -12354,7 +12423,6 @@ "node": ">=14.16" }, "peerDependencies": { - "@rescript/core": ">= 1.3.0", "rescript": ">= 11.1.0" } }, @@ -12418,6 +12486,7 @@ "@pgtyped/wire": "^2.2.0", "chalk": "^4.1.0", "debug": "^4.1.1", + "pgsql-ast-parser": "^12.0.1", "pgtyped-rescript-runtime": "^2.2.0" }, "devDependencies": { @@ -15587,6 +15656,11 @@ "path-type": "^4.0.0" } }, + "discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==" + }, "dotenv": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", @@ -18332,6 +18406,11 @@ "resolved": "https://registry.npmjs.org/mongodb-uri/-/mongodb-uri-0.9.7.tgz", "integrity": "sha512-s6BdnqNoEYfViPJgkH85X5Nw5NpzxN8hoflKLweNa7vBxt2V7kaS06d74pAtqDxde8fn4r9h4dNdLiFGoNV0KA==" }, + "moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -18395,6 +18474,24 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "requires": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + } + } + }, "negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -19673,12 +19770,20 @@ } } }, + "pgsql-ast-parser": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/pgsql-ast-parser/-/pgsql-ast-parser-12.0.1.tgz", + "integrity": "sha512-pe8C6Zh5MsS+o38WlSu18NhrTjAv1UNMeDTs2/Km2ZReZdYBYtwtbWGZKK2BM2izv5CrQpbmP0oI10wvHOwv4A==", + "requires": { + "moo": "^0.5.1", + "nearley": "^2.19.5" + } + }, "pgtyped-rescript": { "version": "file:packages/cli", "requires": { "@pgtyped/parser": "^2.1.0", "@pgtyped/wire": "^2.2.0", - "@rescript/core": "1.6.0", "@rescript/tools": "0.6.4", "@types/debug": "4.1.8", "@types/fs-extra": "11.0.1", @@ -19712,6 +19817,7 @@ "@types/debug": "^4.1.4", "chalk": "^4.1.0", "debug": "^4.1.1", + "pgsql-ast-parser": "^12.0.1", "pgtyped-rescript-runtime": "^2.2.0" } }, @@ -19898,6 +20004,20 @@ "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true }, + "railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==" + }, + "randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "requires": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + } + }, "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -20256,6 +20376,11 @@ "signal-exit": "^3.0.2" } }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, "retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", diff --git a/packages/cli/package.json b/packages/cli/package.json index 074967b1..fca506f4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -39,7 +39,6 @@ "@pgtyped/parser": "^2.1.0", "@pgtyped/wire": "^2.2.0", "@rescript/tools": "0.6.4", - "@rescript/core": "1.6.0", "camel-case": "^4.1.1", "chalk": "^4.0.0", "chokidar": "^3.3.1", @@ -66,7 +65,6 @@ "rescript": "11.1.0" }, "peerDependencies": { - "@rescript/core": ">= 1.3.0", "rescript": ">= 11.1.0" } } diff --git a/packages/cli/rescript.json b/packages/cli/rescript.json index 2936429a..22d78b1c 100644 --- a/packages/cli/rescript.json +++ b/packages/cli/rescript.json @@ -7,7 +7,5 @@ "module": "commonjs", "in-source": true }, - "suffix": ".js", - "bs-dependencies": ["@rescript/core"], - "bsc-flags": ["-open RescriptCore"] + "suffix": ".js" } diff --git a/packages/cli/src/generator.ts b/packages/cli/src/generator.ts index 3553bdf1..87855b36 100644 --- a/packages/cli/src/generator.ts +++ b/packages/cli/src/generator.ts @@ -135,31 +135,48 @@ export async function queryToTypeDeclarations( const paramFieldTypes: IField[] = []; const records: string[] = []; - returnTypes.forEach(({ returnName, type, nullable, comment }) => { - let tsTypeName = types.use(type, TypeScope.Return); + returnTypes.forEach( + ({ returnName, type, nullable, comment, checkValues }) => { + let tsTypeName = types.use(type, TypeScope.Return); + + if (checkValues != null && checkValues.length > 0) { + tsTypeName = `[${checkValues + .map((v) => { + switch (v.type) { + case 'string': + return `#"${v.value}"`; + case 'integer': + return `#${v.value}`; + case 'float': + return `#${v.value}`; + } + }) + .join(' | ')}]`; + } - const lastCharacter = returnName[returnName.length - 1]; // Checking for type hints - const addNullability = lastCharacter === '?'; - const removeNullability = lastCharacter === '!'; - if ( - (addNullability || nullable || nullable == null) && - !removeNullability - ) { - tsTypeName = 'option<' + tsTypeName + '>'; - } + const lastCharacter = returnName[returnName.length - 1]; // Checking for type hints + const addNullability = lastCharacter === '?'; + const removeNullability = lastCharacter === '!'; + if ( + (addNullability || nullable || nullable == null) && + !removeNullability + ) { + tsTypeName = 'option<' + tsTypeName + '>'; + } - if (addNullability || removeNullability) { - returnName = returnName.slice(0, -1); - } + if (addNullability || removeNullability) { + returnName = returnName.slice(0, -1); + } - returnFieldTypes.push({ - fieldName: config.camelCaseColumnNames - ? camelCase(returnName) - : returnName, - fieldType: tsTypeName, - comment, - }); - }); + returnFieldTypes.push({ + fieldName: config.camelCaseColumnNames + ? camelCase(returnName) + : returnName, + fieldType: tsTypeName, + comment, + }); + }, + ); const { params } = paramMetadata; for (const param of paramMetadata.mapping) { diff --git a/packages/cli/src/res/PgTyped.res b/packages/cli/src/res/PgTyped.res index 5a1016b0..a5d71396 100644 --- a/packages/cli/src/res/PgTyped.res +++ b/packages/cli/src/res/PgTyped.res @@ -13,7 +13,7 @@ module Pg = { rows: array<'row>, fields: array, command: string, - rowCount: Null.t, + rowCount: Js.Null.t, } } diff --git a/packages/example/docker-compose.yml b/packages/example/docker-compose.yml index 8c1f0497..c78db030 100644 --- a/packages/example/docker-compose.yml +++ b/packages/example/docker-compose.yml @@ -5,6 +5,7 @@ services: db: image: postgres:15-alpine restart: always + container_name: pgtyped_db environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: password @@ -18,6 +19,7 @@ services: context: . args: NODE_IMAGE: "node:${NODE_VERSION:-18}-alpine" + container_name: pgtyped_build environment: PGHOST: db PGUSER: postgres @@ -32,27 +34,44 @@ services: condition: none depends_on: - db - command: sh -c "node /app/packages/cli/lib/index.js --config config.json && ./check-git-diff.sh" + command: sh -c "echo 'Cleaning database...' && psql -h db -U postgres -d postgres -c 'DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO public;' && echo 'Reingesting schema.sql...' && psql -h db -U postgres -d postgres -f sql/schema.sql && echo 'Schema ingested successfully!' && node /app/packages/cli/lib/index.js --config config.json && ./check-git-diff.sh" watch: <<: *build + container_name: pgtyped_watch command: npx pgtyped --watch --config config.json test: <<: *build + container_name: pgtyped_test depends_on: - build command: npx jest rescript.test.js test-cjs: <<: *build + container_name: pgtyped_test_cjs depends_on: - build command: npx jest -c jest-cjs.config.ts rescript.test.js test-update-snapshots: <<: *build + container_name: pgtyped_test_snapshots depends_on: - build command: npx jest rescript.test.js -u + # Persistent build container for reuse + build-persistent: + <<: *build + container_name: pgtyped_build_persistent + command: tail -f /dev/null # Keep container running + restart: unless-stopped + + # Helper service to reingest schema on demand + reingest-schema: + <<: *build + container_name: pgtyped_reingest_schema + command: sh -c "echo 'Cleaning database...' && psql -h db -U postgres -d postgres -c 'DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO public;' && echo 'Reingesting schema.sql...' && psql -h db -U postgres -d postgres -f sql/schema.sql && echo 'Schema ingested successfully!'" + diff --git a/packages/example/sql/schema.sql b/packages/example/sql/schema.sql index 80193525..6b2137ff 100644 --- a/packages/example/sql/schema.sql +++ b/packages/example/sql/schema.sql @@ -32,9 +32,7 @@ CREATE TABLE books ( rank INTEGER, name TEXT, author_id INTEGER REFERENCES authors, - categories category[], - meta jsonb[], - big_int bigint + categories category[] ); CREATE TABLE book_comments ( @@ -44,6 +42,40 @@ CREATE TABLE book_comments ( body TEXT ); +CREATE TABLE dump( + id SERIAL PRIMARY KEY, + meta jsonb[], + big_int bigint, + + -- Check constraints that SHOULD be supported + some_string_enum text check (some_string_enum in ('FIRST', 'second', 'Third', 'fourth')), + some_int_enum integer check (some_int_enum in (1, 2, 3, 4)), + some_float_enum float check (some_float_enum in (1.5, 2.5, 3.5, 4.5)), + status TEXT CHECK (status IN ('published', 'draft', 'archived')), + format TEXT CHECK (format = ANY (ARRAY['hardcover'::text, 'paperback'::text, 'ebook'::text, 'audiobook'::text])), + language TEXT CHECK (language IN ('en', 'es', 'fr', 'de')), + page_count INTEGER CHECK (page_count IN (100, 200, 300, 400, 500)), + priority INTEGER CHECK (priority = ANY (ARRAY[1, 2, 3, 4, 5])), + price DECIMAL(10,2) CHECK (price IN (9.99, 19.99, 29.99, 39.99)), + rating FLOAT CHECK (rating = ANY (ARRAY[1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0])), + discount_rate FLOAT CHECK (discount_rate IN (0.05, 0.10, 0.15, 0.20, 0.25)), + weight_kg FLOAT CHECK (weight_kg IN (0.1, 0.2, 0.5, 1.0, 1.5, 2.0)), + binding_type TEXT CHECK (binding_type IN ('Hardcover', 'Paperback', 'SPIRAL', 'loose-leaf')), + + -- ! Check constraints that can't be supported right now + -- Not meaningful to represent ranges in ReScript + edition INTEGER CHECK (edition BETWEEN 1 AND 10), + -- Mixed value type constraints + availability TEXT CHECK (availability IN ('in-stock', 'limited', 'out-of-stock') OR availability = 'special-order'), + -- Boolean-like constraints + is_featured BOOLEAN CHECK (is_featured IN (true, false)), + -- Range constraints with specific values + publication_year INTEGER CHECK (publication_year IN (2020, 2021, 2022, 2023, 2024) OR publication_year BETWEEN 1900 AND 2030), + -- Complex expressions + isbn TEXT CHECK (length(isbn) = 13 AND isbn ~ '^[0-9]+$'), + json_test jsonb +); + INSERT INTO users (email, user_name, first_name, last_name, age) VALUES ('alex.doe@example.com', 'alexd', 'Alex', 'Doe', 35), ('jane.holmes@example.com', 'jane67', 'Jane', 'Holmes', 23), diff --git a/packages/example/src/__snapshots__/rescript.test.js.snap b/packages/example/src/__snapshots__/rescript.test.js.snap index bff6c41e..90675285 100644 --- a/packages/example/src/__snapshots__/rescript.test.js.snap +++ b/packages/example/src/__snapshots__/rescript.test.js.snap @@ -51,10 +51,8 @@ exports[`select query with join and a parameter override 1`] = ` Array [ Object { "author_id": 2, - "big_int": undefined, "categories": undefined, "id": 2, - "meta": undefined, "name": "The Dragons Of Eden", "rank": 4, }, diff --git a/packages/example/src/books/BookService.res b/packages/example/src/books/BookService.res index 698f597b..9fd3ae50 100644 --- a/packages/example/src/books/BookService.res +++ b/packages/example/src/books/BookService.res @@ -9,6 +9,7 @@ let findBookById = (client, ~id) => { let booksByAuthor = (client, ~authorName) => { let query = %sql.many(` + /* @name BooksByAuthor */ SELECT b.* FROM books b INNER JOIN authors a ON a.id = b.author_id WHERE a.first_name || ' ' || a.last_name = :authorName!; @@ -16,15 +17,3 @@ let booksByAuthor = (client, ~authorName) => { client->query({authorName: authorName}) } - -let queryWithParams = %sql.one(` - /* - @param notification -> (payload, user_id, type) - */ - INSERT INTO notifications (payload, user_id, type) VALUES :notification -`) - -let queryWithParamsSingleLine = %sql.one(` - /* @param notification -> (payload, user_id, type) */ - INSERT INTO notifications (payload, user_id, type) VALUES :notification -`) diff --git a/packages/example/src/books/BookServiceParams.res b/packages/example/src/books/BookServiceParams.res new file mode 100644 index 00000000..bd46874d --- /dev/null +++ b/packages/example/src/books/BookServiceParams.res @@ -0,0 +1,6 @@ +let queryWithParams = %sql.one(` + /* + @param notification -> (payload, user_id, type) + */ + INSERT INTO notifications (payload, user_id, type) VALUES :notification +`) diff --git a/packages/example/src/books/BookServiceParams2.res b/packages/example/src/books/BookServiceParams2.res new file mode 100644 index 00000000..9d82442b --- /dev/null +++ b/packages/example/src/books/BookServiceParams2.res @@ -0,0 +1,4 @@ +let queryWithParamsSingleLine = %sql.one(` + /* @param notification -> (payload, user_id, type) */ + INSERT INTO notifications (payload, user_id, type) VALUES :notification +`) diff --git a/packages/example/src/books/BookServiceParams2__sql.gen.tsx b/packages/example/src/books/BookServiceParams2__sql.gen.tsx new file mode 100644 index 00000000..1bc32cc5 --- /dev/null +++ b/packages/example/src/books/BookServiceParams2__sql.gen.tsx @@ -0,0 +1,52 @@ +/* TypeScript file generated from BookServiceParams2__sql.res by genType. */ + +/* eslint-disable */ +/* tslint:disable */ + +const BookServiceParams2__sqlJS = require('./BookServiceParams2__sql.js'); + +import type {Pg_Client_t as PgTyped_Pg_Client_t} from 'pgtyped-rescript/src/res/PgTyped.gen'; + +import type {t as JSON_t} from './JSON.gen'; + +export type notification_type = "deadline" | "notification" | "reminder"; + +export type query1Params_notification = { + readonly payload?: JSON_t; + readonly user_id?: number; + readonly type: (undefined | notification_type) +}; + +/** 'Query1' parameters type */ +export type query1Params = { readonly notification: query1Params_notification }; + +/** 'Query1' return type */ +export type query1Result = void; + +/** 'Query1' query type */ +export type query1Query = { readonly params: query1Params; readonly result: query1Result }; + +/** Returns an array of all matched results. */ +export const Query1_many: (_1:PgTyped_Pg_Client_t, _2:query1Params) => Promise = BookServiceParams2__sqlJS.Query1.many as any; + +/** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ +export const Query1_one: (_1:PgTyped_Pg_Client_t, _2:query1Params) => Promise<(undefined | query1Result)> = BookServiceParams2__sqlJS.Query1.one as any; + +/** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ +export const Query1_expectOne: (_1:PgTyped_Pg_Client_t, _2:query1Params, errorMessage:(undefined | string)) => Promise = BookServiceParams2__sqlJS.Query1.expectOne as any; + +/** Executes the query, but ignores whatever is returned by it. */ +export const Query1_execute: (_1:PgTyped_Pg_Client_t, _2:query1Params) => Promise = BookServiceParams2__sqlJS.Query1.execute as any; + +export const query1: (params:query1Params, client:PgTyped_Pg_Client_t) => Promise = BookServiceParams2__sqlJS.query1 as any; + +export const Query1: { + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + expectOne: (_1:PgTyped_Pg_Client_t, _2:query1Params, errorMessage:(undefined | string)) => Promise; + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + one: (_1:PgTyped_Pg_Client_t, _2:query1Params) => Promise<(undefined | query1Result)>; + /** Returns an array of all matched results. */ + many: (_1:PgTyped_Pg_Client_t, _2:query1Params) => Promise; + /** Executes the query, but ignores whatever is returned by it. */ + execute: (_1:PgTyped_Pg_Client_t, _2:query1Params) => Promise +} = BookServiceParams2__sqlJS.Query1 as any; diff --git a/packages/example/src/books/BookServiceParams2__sql.res b/packages/example/src/books/BookServiceParams2__sql.res new file mode 100644 index 00000000..0be41ac6 --- /dev/null +++ b/packages/example/src/books/BookServiceParams2__sql.res @@ -0,0 +1,90 @@ +/** Types generated for queries found in "src/books/BookServiceParams2.res" */ +open PgTyped + + +@gentype +type notification_type = [#"deadline" | #"notification" | #"reminder"] + +@gentype +type query1Params_notification = { + payload?: JSON.t, + user_id?: int, + @as("type") type_?: notification_type +} +/** 'Query1' parameters type */ +@gentype +type query1Params = { + notification: query1Params_notification, +} + +/** 'Query1' return type */ +@gentype +type query1Result = unit + +/** 'Query1' query type */ +@gentype +type query1Query = { + params: query1Params, + result: query1Result, +} + +%%private(let query1IR: IR.t = %raw(`{"usedParamSet":{"notification":true},"params":[{"name":"notification","required":false,"transform":{"type":"pick_tuple","keys":[{"name":"payload","required":false},{"name":"user_id","required":false},{"name":"type","required":false}]},"locs":[{"a":58,"b":70}]}],"statement":"INSERT INTO notifications (payload, user_id, type) VALUES :notification"}`)) + +/** + Runnable query: + ```sql +INSERT INTO notifications (payload, user_id, type) VALUES ($1,$2,$3) + ``` + + */ +@gentype +module Query1: { + /** Returns an array of all matched results. */ + @gentype + let many: (PgTyped.Pg.Client.t, query1Params) => promise> + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + @gentype + let one: (PgTyped.Pg.Client.t, query1Params) => promise> + + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + @gentype + let expectOne: ( + PgTyped.Pg.Client.t, + query1Params, + ~errorMessage: string=? + ) => promise + + /** Executes the query, but ignores whatever is returned by it. */ + @gentype + let execute: (PgTyped.Pg.Client.t, query1Params) => promise +} = { + @module("pgtyped-rescript-runtime") @new external query1: IR.t => PreparedStatement.t = "PreparedQuery"; + let query = query1(query1IR) + let query = (params, ~client) => query->PreparedStatement.run(params, ~client) + + @gentype + let many = (client, params) => query(params, ~client) + + @gentype + let one = async (client, params) => switch await query(params, ~client) { + | [item] => Some(item) + | _ => None + } + + @gentype + let expectOne = async (client, params, ~errorMessage=?) => switch await query(params, ~client) { + | [item] => item + | _ => panic(errorMessage->Option.getOr("More or less than one item was returned")) + } + + @gentype + let execute = async (client, params) => { + let _ = await query(params, ~client) + } +} + +@gentype +@deprecated("Use 'Query1.many' directly instead") +let query1 = (params, ~client) => Query1.many(client, params) + + diff --git a/packages/example/src/books/BookServiceParams__sql.gen.tsx b/packages/example/src/books/BookServiceParams__sql.gen.tsx new file mode 100644 index 00000000..baca139f --- /dev/null +++ b/packages/example/src/books/BookServiceParams__sql.gen.tsx @@ -0,0 +1,52 @@ +/* TypeScript file generated from BookServiceParams__sql.res by genType. */ + +/* eslint-disable */ +/* tslint:disable */ + +const BookServiceParams__sqlJS = require('./BookServiceParams__sql.js'); + +import type {Pg_Client_t as PgTyped_Pg_Client_t} from 'pgtyped-rescript/src/res/PgTyped.gen'; + +import type {t as JSON_t} from './JSON.gen'; + +export type notification_type = "deadline" | "notification" | "reminder"; + +export type query1Params_notification = { + readonly payload?: JSON_t; + readonly user_id?: number; + readonly type: (undefined | notification_type) +}; + +/** 'Query1' parameters type */ +export type query1Params = { readonly notification: query1Params_notification }; + +/** 'Query1' return type */ +export type query1Result = void; + +/** 'Query1' query type */ +export type query1Query = { readonly params: query1Params; readonly result: query1Result }; + +/** Returns an array of all matched results. */ +export const Query1_many: (_1:PgTyped_Pg_Client_t, _2:query1Params) => Promise = BookServiceParams__sqlJS.Query1.many as any; + +/** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ +export const Query1_one: (_1:PgTyped_Pg_Client_t, _2:query1Params) => Promise<(undefined | query1Result)> = BookServiceParams__sqlJS.Query1.one as any; + +/** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ +export const Query1_expectOne: (_1:PgTyped_Pg_Client_t, _2:query1Params, errorMessage:(undefined | string)) => Promise = BookServiceParams__sqlJS.Query1.expectOne as any; + +/** Executes the query, but ignores whatever is returned by it. */ +export const Query1_execute: (_1:PgTyped_Pg_Client_t, _2:query1Params) => Promise = BookServiceParams__sqlJS.Query1.execute as any; + +export const query1: (params:query1Params, client:PgTyped_Pg_Client_t) => Promise = BookServiceParams__sqlJS.query1 as any; + +export const Query1: { + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + expectOne: (_1:PgTyped_Pg_Client_t, _2:query1Params, errorMessage:(undefined | string)) => Promise; + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + one: (_1:PgTyped_Pg_Client_t, _2:query1Params) => Promise<(undefined | query1Result)>; + /** Returns an array of all matched results. */ + many: (_1:PgTyped_Pg_Client_t, _2:query1Params) => Promise; + /** Executes the query, but ignores whatever is returned by it. */ + execute: (_1:PgTyped_Pg_Client_t, _2:query1Params) => Promise +} = BookServiceParams__sqlJS.Query1 as any; diff --git a/packages/example/src/books/BookServiceParams__sql.res b/packages/example/src/books/BookServiceParams__sql.res new file mode 100644 index 00000000..c7284256 --- /dev/null +++ b/packages/example/src/books/BookServiceParams__sql.res @@ -0,0 +1,90 @@ +/** Types generated for queries found in "src/books/BookServiceParams.res" */ +open PgTyped + + +@gentype +type notification_type = [#"deadline" | #"notification" | #"reminder"] + +@gentype +type query1Params_notification = { + payload?: JSON.t, + user_id?: int, + @as("type") type_?: notification_type +} +/** 'Query1' parameters type */ +@gentype +type query1Params = { + notification: query1Params_notification, +} + +/** 'Query1' return type */ +@gentype +type query1Result = unit + +/** 'Query1' query type */ +@gentype +type query1Query = { + params: query1Params, + result: query1Result, +} + +%%private(let query1IR: IR.t = %raw(`{"usedParamSet":{"notification":true},"params":[{"name":"notification","required":false,"transform":{"type":"pick_tuple","keys":[{"name":"payload","required":false},{"name":"user_id","required":false},{"name":"type","required":false}]},"locs":[{"a":58,"b":70}]}],"statement":"INSERT INTO notifications (payload, user_id, type) VALUES :notification"}`)) + +/** + Runnable query: + ```sql +INSERT INTO notifications (payload, user_id, type) VALUES ($1,$2,$3) + ``` + + */ +@gentype +module Query1: { + /** Returns an array of all matched results. */ + @gentype + let many: (PgTyped.Pg.Client.t, query1Params) => promise> + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + @gentype + let one: (PgTyped.Pg.Client.t, query1Params) => promise> + + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + @gentype + let expectOne: ( + PgTyped.Pg.Client.t, + query1Params, + ~errorMessage: string=? + ) => promise + + /** Executes the query, but ignores whatever is returned by it. */ + @gentype + let execute: (PgTyped.Pg.Client.t, query1Params) => promise +} = { + @module("pgtyped-rescript-runtime") @new external query1: IR.t => PreparedStatement.t = "PreparedQuery"; + let query = query1(query1IR) + let query = (params, ~client) => query->PreparedStatement.run(params, ~client) + + @gentype + let many = (client, params) => query(params, ~client) + + @gentype + let one = async (client, params) => switch await query(params, ~client) { + | [item] => Some(item) + | _ => None + } + + @gentype + let expectOne = async (client, params, ~errorMessage=?) => switch await query(params, ~client) { + | [item] => item + | _ => panic(errorMessage->Option.getOr("More or less than one item was returned")) + } + + @gentype + let execute = async (client, params) => { + let _ = await query(params, ~client) + } +} + +@gentype +@deprecated("Use 'Query1.many' directly instead") +let query1 = (params, ~client) => Query1.many(client, params) + + diff --git a/packages/example/src/books/BookService__sql.gen.tsx b/packages/example/src/books/BookService__sql.gen.tsx index 4b4c2436..fee7ce98 100644 --- a/packages/example/src/books/BookService__sql.gen.tsx +++ b/packages/example/src/books/BookService__sql.gen.tsx @@ -7,79 +7,53 @@ const BookService__sqlJS = require('./BookService__sql.js'); import type {Pg_Client_t as PgTyped_Pg_Client_t} from 'pgtyped-rescript/src/res/PgTyped.gen'; -import type {t as JSON_t} from './JSON.gen'; - export type category = "novel" | "science-fiction" | "thriller"; -export type notification_type = "deadline" | "notification" | "reminder"; - -export type arrayJSON_t = JSON_t[]; - export type categoryArray = category[]; -/** 'FindBookById' parameters type */ -export type findBookByIdParams = { readonly id?: number }; +/** 'BooksByAuthor' parameters type */ +export type booksByAuthorParams = { readonly authorName: string }; -/** 'FindBookById' return type */ -export type findBookByIdResult = { +/** 'BooksByAuthor' return type */ +export type booksByAuthorResult = { readonly author_id: (undefined | number); - readonly big_int: (undefined | bigint); readonly categories: (undefined | categoryArray); readonly id: number; - readonly meta: (undefined | arrayJSON_t); readonly name: (undefined | string); readonly rank: (undefined | number) }; -/** 'FindBookById' query type */ -export type findBookByIdQuery = { readonly params: findBookByIdParams; readonly result: findBookByIdResult }; +/** 'BooksByAuthor' query type */ +export type booksByAuthorQuery = { readonly params: booksByAuthorParams; readonly result: booksByAuthorResult }; -/** 'Query1' parameters type */ -export type query1Params = { readonly authorName: string }; +/** 'FindBookById' parameters type */ +export type findBookByIdParams = { readonly id?: number }; -/** 'Query1' return type */ -export type query1Result = { +/** 'FindBookById' return type */ +export type findBookByIdResult = { readonly author_id: (undefined | number); - readonly big_int: (undefined | bigint); readonly categories: (undefined | categoryArray); readonly id: number; - readonly meta: (undefined | arrayJSON_t); readonly name: (undefined | string); readonly rank: (undefined | number) }; -/** 'Query1' query type */ -export type query1Query = { readonly params: query1Params; readonly result: query1Result }; - -export type query2Params_notification = { - readonly payload?: JSON_t; - readonly user_id?: number; - readonly type: (undefined | notification_type) -}; - -/** 'Query2' parameters type */ -export type query2Params = { readonly notification: query2Params_notification }; - -/** 'Query2' return type */ -export type query2Result = void; +/** 'FindBookById' query type */ +export type findBookByIdQuery = { readonly params: findBookByIdParams; readonly result: findBookByIdResult }; -/** 'Query2' query type */ -export type query2Query = { readonly params: query2Params; readonly result: query2Result }; +/** Returns an array of all matched results. */ +export const BooksByAuthor_many: (_1:PgTyped_Pg_Client_t, _2:booksByAuthorParams) => Promise = BookService__sqlJS.BooksByAuthor.many as any; -export type query3Params_notification = { - readonly payload?: JSON_t; - readonly user_id?: number; - readonly type: (undefined | notification_type) -}; +/** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ +export const BooksByAuthor_one: (_1:PgTyped_Pg_Client_t, _2:booksByAuthorParams) => Promise<(undefined | booksByAuthorResult)> = BookService__sqlJS.BooksByAuthor.one as any; -/** 'Query3' parameters type */ -export type query3Params = { readonly notification: query3Params_notification }; +/** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ +export const BooksByAuthor_expectOne: (_1:PgTyped_Pg_Client_t, _2:booksByAuthorParams, errorMessage:(undefined | string)) => Promise = BookService__sqlJS.BooksByAuthor.expectOne as any; -/** 'Query3' return type */ -export type query3Result = void; +/** Executes the query, but ignores whatever is returned by it. */ +export const BooksByAuthor_execute: (_1:PgTyped_Pg_Client_t, _2:booksByAuthorParams) => Promise = BookService__sqlJS.BooksByAuthor.execute as any; -/** 'Query3' query type */ -export type query3Query = { readonly params: query3Params; readonly result: query3Result }; +export const booksByAuthor: (params:booksByAuthorParams, client:PgTyped_Pg_Client_t) => Promise = BookService__sqlJS.booksByAuthor as any; /** Returns an array of all matched results. */ export const FindBookById_many: (_1:PgTyped_Pg_Client_t, _2:findBookByIdParams) => Promise = BookService__sqlJS.FindBookById.many as any; @@ -95,48 +69,6 @@ export const FindBookById_execute: (_1:PgTyped_Pg_Client_t, _2:findBookByIdParam export const findBookById: (params:findBookByIdParams, client:PgTyped_Pg_Client_t) => Promise = BookService__sqlJS.findBookById as any; -/** Returns an array of all matched results. */ -export const Query1_many: (_1:PgTyped_Pg_Client_t, _2:query1Params) => Promise = BookService__sqlJS.Query1.many as any; - -/** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ -export const Query1_one: (_1:PgTyped_Pg_Client_t, _2:query1Params) => Promise<(undefined | query1Result)> = BookService__sqlJS.Query1.one as any; - -/** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ -export const Query1_expectOne: (_1:PgTyped_Pg_Client_t, _2:query1Params, errorMessage:(undefined | string)) => Promise = BookService__sqlJS.Query1.expectOne as any; - -/** Executes the query, but ignores whatever is returned by it. */ -export const Query1_execute: (_1:PgTyped_Pg_Client_t, _2:query1Params) => Promise = BookService__sqlJS.Query1.execute as any; - -export const query1: (params:query1Params, client:PgTyped_Pg_Client_t) => Promise = BookService__sqlJS.query1 as any; - -/** Returns an array of all matched results. */ -export const Query2_many: (_1:PgTyped_Pg_Client_t, _2:query2Params) => Promise = BookService__sqlJS.Query2.many as any; - -/** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ -export const Query2_one: (_1:PgTyped_Pg_Client_t, _2:query2Params) => Promise<(undefined | query2Result)> = BookService__sqlJS.Query2.one as any; - -/** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ -export const Query2_expectOne: (_1:PgTyped_Pg_Client_t, _2:query2Params, errorMessage:(undefined | string)) => Promise = BookService__sqlJS.Query2.expectOne as any; - -/** Executes the query, but ignores whatever is returned by it. */ -export const Query2_execute: (_1:PgTyped_Pg_Client_t, _2:query2Params) => Promise = BookService__sqlJS.Query2.execute as any; - -export const query2: (params:query2Params, client:PgTyped_Pg_Client_t) => Promise = BookService__sqlJS.query2 as any; - -/** Returns an array of all matched results. */ -export const Query3_many: (_1:PgTyped_Pg_Client_t, _2:query3Params) => Promise = BookService__sqlJS.Query3.many as any; - -/** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ -export const Query3_one: (_1:PgTyped_Pg_Client_t, _2:query3Params) => Promise<(undefined | query3Result)> = BookService__sqlJS.Query3.one as any; - -/** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ -export const Query3_expectOne: (_1:PgTyped_Pg_Client_t, _2:query3Params, errorMessage:(undefined | string)) => Promise = BookService__sqlJS.Query3.expectOne as any; - -/** Executes the query, but ignores whatever is returned by it. */ -export const Query3_execute: (_1:PgTyped_Pg_Client_t, _2:query3Params) => Promise = BookService__sqlJS.Query3.execute as any; - -export const query3: (params:query3Params, client:PgTyped_Pg_Client_t) => Promise = BookService__sqlJS.query3 as any; - export const FindBookById: { /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ expectOne: (_1:PgTyped_Pg_Client_t, _2:findBookByIdParams, errorMessage:(undefined | string)) => Promise; @@ -148,35 +80,13 @@ export const FindBookById: { execute: (_1:PgTyped_Pg_Client_t, _2:findBookByIdParams) => Promise } = BookService__sqlJS.FindBookById as any; -export const Query1: { - /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ - expectOne: (_1:PgTyped_Pg_Client_t, _2:query1Params, errorMessage:(undefined | string)) => Promise; - /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ - one: (_1:PgTyped_Pg_Client_t, _2:query1Params) => Promise<(undefined | query1Result)>; - /** Returns an array of all matched results. */ - many: (_1:PgTyped_Pg_Client_t, _2:query1Params) => Promise; - /** Executes the query, but ignores whatever is returned by it. */ - execute: (_1:PgTyped_Pg_Client_t, _2:query1Params) => Promise -} = BookService__sqlJS.Query1 as any; - -export const Query2: { - /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ - expectOne: (_1:PgTyped_Pg_Client_t, _2:query2Params, errorMessage:(undefined | string)) => Promise; - /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ - one: (_1:PgTyped_Pg_Client_t, _2:query2Params) => Promise<(undefined | query2Result)>; - /** Returns an array of all matched results. */ - many: (_1:PgTyped_Pg_Client_t, _2:query2Params) => Promise; - /** Executes the query, but ignores whatever is returned by it. */ - execute: (_1:PgTyped_Pg_Client_t, _2:query2Params) => Promise -} = BookService__sqlJS.Query2 as any; - -export const Query3: { +export const BooksByAuthor: { /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ - expectOne: (_1:PgTyped_Pg_Client_t, _2:query3Params, errorMessage:(undefined | string)) => Promise; + expectOne: (_1:PgTyped_Pg_Client_t, _2:booksByAuthorParams, errorMessage:(undefined | string)) => Promise; /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ - one: (_1:PgTyped_Pg_Client_t, _2:query3Params) => Promise<(undefined | query3Result)>; + one: (_1:PgTyped_Pg_Client_t, _2:booksByAuthorParams) => Promise<(undefined | booksByAuthorResult)>; /** Returns an array of all matched results. */ - many: (_1:PgTyped_Pg_Client_t, _2:query3Params) => Promise; + many: (_1:PgTyped_Pg_Client_t, _2:booksByAuthorParams) => Promise; /** Executes the query, but ignores whatever is returned by it. */ - execute: (_1:PgTyped_Pg_Client_t, _2:query3Params) => Promise -} = BookService__sqlJS.Query3 as any; + execute: (_1:PgTyped_Pg_Client_t, _2:booksByAuthorParams) => Promise +} = BookService__sqlJS.BooksByAuthor as any; diff --git a/packages/example/src/books/BookService__sql.res b/packages/example/src/books/BookService__sql.res index cd5ab0d3..c142099a 100644 --- a/packages/example/src/books/BookService__sql.res +++ b/packages/example/src/books/BookService__sql.res @@ -5,72 +5,66 @@ open PgTyped @gentype type category = [#"novel" | #"science-fiction" | #"thriller"] -@gentype -type notification_type = [#"deadline" | #"notification" | #"reminder"] - -@gentype -type arrayJSON_t = array - @gentype type categoryArray = array -/** 'FindBookById' parameters type */ +/** 'BooksByAuthor' parameters type */ @gentype -type findBookByIdParams = { - id?: int, +type booksByAuthorParams = { + authorName: string, } -/** 'FindBookById' return type */ +/** 'BooksByAuthor' return type */ @gentype -type findBookByIdResult = { +type booksByAuthorResult = { author_id: option, - big_int: option, categories: option, id: int, - meta: option, name: option, rank: option, } -/** 'FindBookById' query type */ +/** 'BooksByAuthor' query type */ @gentype -type findBookByIdQuery = { - params: findBookByIdParams, - result: findBookByIdResult, +type booksByAuthorQuery = { + params: booksByAuthorParams, + result: booksByAuthorResult, } -%%private(let findBookByIdIR: IR.t = %raw(`{"usedParamSet":{"id":true},"params":[{"name":"id","required":false,"transform":{"type":"scalar"},"locs":[{"a":31,"b":33}]}],"statement":"SELECT * FROM books WHERE id = :id"}`)) +%%private(let booksByAuthorIR: IR.t = %raw(`{"usedParamSet":{"authorName":true},"params":[{"name":"authorName","required":true,"transform":{"type":"scalar"},"locs":[{"a":118,"b":129}]}],"statement":"SELECT b.* FROM books b\n INNER JOIN authors a ON a.id = b.author_id\n WHERE a.first_name || ' ' || a.last_name = :authorName!"}`)) /** Runnable query: ```sql -SELECT * FROM books WHERE id = $1 +SELECT b.* FROM books b + INNER JOIN authors a ON a.id = b.author_id + WHERE a.first_name || ' ' || a.last_name = $1 ``` */ @gentype -module FindBookById: { +module BooksByAuthor: { /** Returns an array of all matched results. */ @gentype - let many: (PgTyped.Pg.Client.t, findBookByIdParams) => promise> + let many: (PgTyped.Pg.Client.t, booksByAuthorParams) => promise> /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ @gentype - let one: (PgTyped.Pg.Client.t, findBookByIdParams) => promise> + let one: (PgTyped.Pg.Client.t, booksByAuthorParams) => promise> /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ @gentype let expectOne: ( PgTyped.Pg.Client.t, - findBookByIdParams, + booksByAuthorParams, ~errorMessage: string=? - ) => promise + ) => promise /** Executes the query, but ignores whatever is returned by it. */ @gentype - let execute: (PgTyped.Pg.Client.t, findBookByIdParams) => promise + let execute: (PgTyped.Pg.Client.t, booksByAuthorParams) => promise } = { - @module("pgtyped-rescript-runtime") @new external findBookById: IR.t => PreparedStatement.t = "PreparedQuery"; - let query = findBookById(findBookByIdIR) + @module("pgtyped-rescript-runtime") @new external booksByAuthor: IR.t => PreparedStatement.t = "PreparedQuery"; + let query = booksByAuthor(booksByAuthorIR) let query = (params, ~client) => query->PreparedStatement.run(params, ~client) @gentype @@ -95,235 +89,65 @@ module FindBookById: { } @gentype -@deprecated("Use 'FindBookById.many' directly instead") -let findBookById = (params, ~client) => FindBookById.many(client, params) +@deprecated("Use 'BooksByAuthor.many' directly instead") +let booksByAuthor = (params, ~client) => BooksByAuthor.many(client, params) -/** 'Query1' parameters type */ +/** 'FindBookById' parameters type */ @gentype -type query1Params = { - authorName: string, +type findBookByIdParams = { + id?: int, } -/** 'Query1' return type */ +/** 'FindBookById' return type */ @gentype -type query1Result = { +type findBookByIdResult = { author_id: option, - big_int: option, categories: option, id: int, - meta: option, name: option, rank: option, } -/** 'Query1' query type */ -@gentype -type query1Query = { - params: query1Params, - result: query1Result, -} - -%%private(let query1IR: IR.t = %raw(`{"usedParamSet":{"authorName":true},"params":[{"name":"authorName","required":true,"transform":{"type":"scalar"},"locs":[{"a":118,"b":129}]}],"statement":"SELECT b.* FROM books b\n INNER JOIN authors a ON a.id = b.author_id\n WHERE a.first_name || ' ' || a.last_name = :authorName!"}`)) - -/** - Runnable query: - ```sql -SELECT b.* FROM books b - INNER JOIN authors a ON a.id = b.author_id - WHERE a.first_name || ' ' || a.last_name = $1 - ``` - - */ -@gentype -module Query1: { - /** Returns an array of all matched results. */ - @gentype - let many: (PgTyped.Pg.Client.t, query1Params) => promise> - /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ - @gentype - let one: (PgTyped.Pg.Client.t, query1Params) => promise> - - /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ - @gentype - let expectOne: ( - PgTyped.Pg.Client.t, - query1Params, - ~errorMessage: string=? - ) => promise - - /** Executes the query, but ignores whatever is returned by it. */ - @gentype - let execute: (PgTyped.Pg.Client.t, query1Params) => promise -} = { - @module("pgtyped-rescript-runtime") @new external query1: IR.t => PreparedStatement.t = "PreparedQuery"; - let query = query1(query1IR) - let query = (params, ~client) => query->PreparedStatement.run(params, ~client) - - @gentype - let many = (client, params) => query(params, ~client) - - @gentype - let one = async (client, params) => switch await query(params, ~client) { - | [item] => Some(item) - | _ => None - } - - @gentype - let expectOne = async (client, params, ~errorMessage=?) => switch await query(params, ~client) { - | [item] => item - | _ => panic(errorMessage->Option.getOr("More or less than one item was returned")) - } - - @gentype - let execute = async (client, params) => { - let _ = await query(params, ~client) - } -} - -@gentype -@deprecated("Use 'Query1.many' directly instead") -let query1 = (params, ~client) => Query1.many(client, params) - - -@gentype -type query2Params_notification = { - payload?: JSON.t, - user_id?: int, - @as("type") type_?: notification_type -} -/** 'Query2' parameters type */ -@gentype -type query2Params = { - notification: query2Params_notification, -} - -/** 'Query2' return type */ -@gentype -type query2Result = unit - -/** 'Query2' query type */ -@gentype -type query2Query = { - params: query2Params, - result: query2Result, -} - -%%private(let query2IR: IR.t = %raw(`{"usedParamSet":{"notification":true},"params":[{"name":"notification","required":false,"transform":{"type":"pick_tuple","keys":[{"name":"payload","required":false},{"name":"user_id","required":false},{"name":"type","required":false}]},"locs":[{"a":58,"b":70}]}],"statement":"INSERT INTO notifications (payload, user_id, type) VALUES :notification"}`)) - -/** - Runnable query: - ```sql -INSERT INTO notifications (payload, user_id, type) VALUES ($1,$2,$3) - ``` - - */ -@gentype -module Query2: { - /** Returns an array of all matched results. */ - @gentype - let many: (PgTyped.Pg.Client.t, query2Params) => promise> - /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ - @gentype - let one: (PgTyped.Pg.Client.t, query2Params) => promise> - - /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ - @gentype - let expectOne: ( - PgTyped.Pg.Client.t, - query2Params, - ~errorMessage: string=? - ) => promise - - /** Executes the query, but ignores whatever is returned by it. */ - @gentype - let execute: (PgTyped.Pg.Client.t, query2Params) => promise -} = { - @module("pgtyped-rescript-runtime") @new external query2: IR.t => PreparedStatement.t = "PreparedQuery"; - let query = query2(query2IR) - let query = (params, ~client) => query->PreparedStatement.run(params, ~client) - - @gentype - let many = (client, params) => query(params, ~client) - - @gentype - let one = async (client, params) => switch await query(params, ~client) { - | [item] => Some(item) - | _ => None - } - - @gentype - let expectOne = async (client, params, ~errorMessage=?) => switch await query(params, ~client) { - | [item] => item - | _ => panic(errorMessage->Option.getOr("More or less than one item was returned")) - } - - @gentype - let execute = async (client, params) => { - let _ = await query(params, ~client) - } -} - -@gentype -@deprecated("Use 'Query2.many' directly instead") -let query2 = (params, ~client) => Query2.many(client, params) - - -@gentype -type query3Params_notification = { - payload?: JSON.t, - user_id?: int, - @as("type") type_?: notification_type -} -/** 'Query3' parameters type */ -@gentype -type query3Params = { - notification: query3Params_notification, -} - -/** 'Query3' return type */ -@gentype -type query3Result = unit - -/** 'Query3' query type */ +/** 'FindBookById' query type */ @gentype -type query3Query = { - params: query3Params, - result: query3Result, +type findBookByIdQuery = { + params: findBookByIdParams, + result: findBookByIdResult, } -%%private(let query3IR: IR.t = %raw(`{"usedParamSet":{"notification":true},"params":[{"name":"notification","required":false,"transform":{"type":"pick_tuple","keys":[{"name":"payload","required":false},{"name":"user_id","required":false},{"name":"type","required":false}]},"locs":[{"a":58,"b":70}]}],"statement":"INSERT INTO notifications (payload, user_id, type) VALUES :notification"}`)) +%%private(let findBookByIdIR: IR.t = %raw(`{"usedParamSet":{"id":true},"params":[{"name":"id","required":false,"transform":{"type":"scalar"},"locs":[{"a":31,"b":33}]}],"statement":"SELECT * FROM books WHERE id = :id"}`)) /** Runnable query: ```sql -INSERT INTO notifications (payload, user_id, type) VALUES ($1,$2,$3) +SELECT * FROM books WHERE id = $1 ``` */ @gentype -module Query3: { +module FindBookById: { /** Returns an array of all matched results. */ @gentype - let many: (PgTyped.Pg.Client.t, query3Params) => promise> + let many: (PgTyped.Pg.Client.t, findBookByIdParams) => promise> /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ @gentype - let one: (PgTyped.Pg.Client.t, query3Params) => promise> + let one: (PgTyped.Pg.Client.t, findBookByIdParams) => promise> /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ @gentype let expectOne: ( PgTyped.Pg.Client.t, - query3Params, + findBookByIdParams, ~errorMessage: string=? - ) => promise + ) => promise /** Executes the query, but ignores whatever is returned by it. */ @gentype - let execute: (PgTyped.Pg.Client.t, query3Params) => promise + let execute: (PgTyped.Pg.Client.t, findBookByIdParams) => promise } = { - @module("pgtyped-rescript-runtime") @new external query3: IR.t => PreparedStatement.t = "PreparedQuery"; - let query = query3(query3IR) + @module("pgtyped-rescript-runtime") @new external findBookById: IR.t => PreparedStatement.t = "PreparedQuery"; + let query = findBookById(findBookByIdIR) let query = (params, ~client) => query->PreparedStatement.run(params, ~client) @gentype @@ -348,7 +172,7 @@ module Query3: { } @gentype -@deprecated("Use 'Query3.many' directly instead") -let query3 = (params, ~client) => Query3.many(client, params) +@deprecated("Use 'FindBookById.many' directly instead") +let findBookById = (params, ~client) => FindBookById.many(client, params) diff --git a/packages/example/src/books/Dump.res b/packages/example/src/books/Dump.res new file mode 100644 index 00000000..d1c4a1d3 --- /dev/null +++ b/packages/example/src/books/Dump.res @@ -0,0 +1,4 @@ +let query = %sql.one(` + /* @name Dump */ + SELECT * FROM dump LIMIT 1; +`) diff --git a/packages/example/src/books/Dump__sql.gen.tsx b/packages/example/src/books/Dump__sql.gen.tsx new file mode 100644 index 00000000..ae4e3453 --- /dev/null +++ b/packages/example/src/books/Dump__sql.gen.tsx @@ -0,0 +1,102 @@ +/* TypeScript file generated from Dump__sql.res by genType. */ + +/* eslint-disable */ +/* tslint:disable */ + +const Dump__sqlJS = require('./Dump__sql.js'); + +import type {Pg_Client_t as PgTyped_Pg_Client_t} from 'pgtyped-rescript/src/res/PgTyped.gen'; + +import type {t as JSON_t} from './JSON.gen'; + +export type arrayJSON_t = JSON_t[]; + +/** 'Dump' parameters type */ +export type dumpParams = void; + +/** 'Dump' return type */ +export type dumpResult = { + readonly availability: (undefined | string); + readonly big_int: (undefined | bigint); + readonly binding_type: (undefined | ( + "SPIRAL" + | "Hardcover" + | "loose-leaf" + | "Paperback")); + readonly discount_rate: (undefined | number); + readonly edition: (undefined | number); + readonly format: (undefined | ( + "hardcover" + | "paperback" + | "ebook" + | "audiobook")); + readonly id: number; + readonly is_featured: (undefined | boolean); + readonly isbn: (undefined | string); + readonly json_test: (undefined | JSON_t); + readonly language: (undefined | ( + "en" + | "de" + | "fr" + | "es")); + readonly meta: (undefined | arrayJSON_t); + readonly page_count: (undefined | ( + 400 + | 100 + | 300 + | 500 + | 200)); + readonly price: (undefined | string); + readonly priority: (undefined | ( + 5 + | 2 + | 1 + | 4 + | 3)); + readonly publication_year: (undefined | number); + readonly rating: (undefined | number); + readonly some_float_enum: (undefined | number); + readonly some_int_enum: (undefined | ( + 2 + | 1 + | 4 + | 3)); + readonly some_string_enum: (undefined | ( + "Third" + | "second" + | "fourth" + | "FIRST")); + readonly status: (undefined | ( + "draft" + | "published" + | "archived")); + readonly weight_kg: (undefined | number) +}; + +/** 'Dump' query type */ +export type dumpQuery = { readonly params: dumpParams; readonly result: dumpResult }; + +/** Returns an array of all matched results. */ +export const Dump_many: (_1:PgTyped_Pg_Client_t, _2:dumpParams) => Promise = Dump__sqlJS.Dump.many as any; + +/** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ +export const Dump_one: (_1:PgTyped_Pg_Client_t, _2:dumpParams) => Promise<(undefined | dumpResult)> = Dump__sqlJS.Dump.one as any; + +/** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ +export const Dump_expectOne: (_1:PgTyped_Pg_Client_t, _2:dumpParams, errorMessage:(undefined | string)) => Promise = Dump__sqlJS.Dump.expectOne as any; + +/** Executes the query, but ignores whatever is returned by it. */ +export const Dump_execute: (_1:PgTyped_Pg_Client_t, _2:dumpParams) => Promise = Dump__sqlJS.Dump.execute as any; + +export const dump: (params:dumpParams, client:PgTyped_Pg_Client_t) => Promise = Dump__sqlJS.dump as any; + +export const Dump: { + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + expectOne: (_1:PgTyped_Pg_Client_t, _2:dumpParams, errorMessage:(undefined | string)) => Promise; + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + one: (_1:PgTyped_Pg_Client_t, _2:dumpParams) => Promise<(undefined | dumpResult)>; + /** Returns an array of all matched results. */ + many: (_1:PgTyped_Pg_Client_t, _2:dumpParams) => Promise; + /** Executes the query, but ignores whatever is returned by it. */ + execute: (_1:PgTyped_Pg_Client_t, _2:dumpParams) => Promise +} = Dump__sqlJS.Dump as any; diff --git a/packages/example/src/books/Dump__sql.res b/packages/example/src/books/Dump__sql.res new file mode 100644 index 00000000..0bb997c5 --- /dev/null +++ b/packages/example/src/books/Dump__sql.res @@ -0,0 +1,105 @@ +/** Types generated for queries found in "src/books/Dump.res" */ +open PgTyped + + +@gentype +type arrayJSON_t = array + +/** 'Dump' parameters type */ +@gentype +type dumpParams = unit + +/** 'Dump' return type */ +@gentype +type dumpResult = { + availability: option, + big_int: option, + binding_type: option<[#"Hardcover" | #"Paperback" | #"SPIRAL" | #"loose-leaf"]>, + discount_rate: option, + edition: option, + format: option<[#"hardcover" | #"paperback" | #"ebook" | #"audiobook"]>, + id: int, + is_featured: option, + isbn: option, + json_test: option, + language: option<[#"en" | #"es" | #"fr" | #"de"]>, + meta: option, + page_count: option<[#100 | #200 | #300 | #400 | #500]>, + price: option, + priority: option<[#1 | #2 | #3 | #4 | #5]>, + publication_year: option, + rating: option, + some_float_enum: option, + some_int_enum: option<[#1 | #2 | #3 | #4]>, + some_string_enum: option<[#"FIRST" | #"second" | #"Third" | #"fourth"]>, + status: option<[#"published" | #"draft" | #"archived"]>, + weight_kg: option, +} + +/** 'Dump' query type */ +@gentype +type dumpQuery = { + params: dumpParams, + result: dumpResult, +} + +%%private(let dumpIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"SELECT * FROM dump LIMIT 1"}`)) + +/** + Runnable query: + ```sql +SELECT * FROM dump LIMIT 1 + ``` + + */ +@gentype +module Dump: { + /** Returns an array of all matched results. */ + @gentype + let many: (PgTyped.Pg.Client.t, dumpParams) => promise> + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + @gentype + let one: (PgTyped.Pg.Client.t, dumpParams) => promise> + + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + @gentype + let expectOne: ( + PgTyped.Pg.Client.t, + dumpParams, + ~errorMessage: string=? + ) => promise + + /** Executes the query, but ignores whatever is returned by it. */ + @gentype + let execute: (PgTyped.Pg.Client.t, dumpParams) => promise +} = { + @module("pgtyped-rescript-runtime") @new external dump: IR.t => PreparedStatement.t = "PreparedQuery"; + let query = dump(dumpIR) + let query = (params, ~client) => query->PreparedStatement.run(params, ~client) + + @gentype + let many = (client, params) => query(params, ~client) + + @gentype + let one = async (client, params) => switch await query(params, ~client) { + | [item] => Some(item) + | _ => None + } + + @gentype + let expectOne = async (client, params, ~errorMessage=?) => switch await query(params, ~client) { + | [item] => item + | _ => panic(errorMessage->Option.getOr("More or less than one item was returned")) + } + + @gentype + let execute = async (client, params) => { + let _ = await query(params, ~client) + } +} + +@gentype +@deprecated("Use 'Dump.many' directly instead") +let dump = (params, ~client) => Dump.many(client, params) + + diff --git a/packages/example/src/books/Json.res b/packages/example/src/books/Json.res new file mode 100644 index 00000000..4394dd8b --- /dev/null +++ b/packages/example/src/books/Json.res @@ -0,0 +1,4 @@ +let query = %sql.one(` + /* @name Json */ + SELECT json_build_object('key', 'value') AS json_object; +`) diff --git a/packages/example/src/books/Json__sql.gen.tsx b/packages/example/src/books/Json__sql.gen.tsx new file mode 100644 index 00000000..fce68a07 --- /dev/null +++ b/packages/example/src/books/Json__sql.gen.tsx @@ -0,0 +1,44 @@ +/* TypeScript file generated from Json__sql.res by genType. */ + +/* eslint-disable */ +/* tslint:disable */ + +const Json__sqlJS = require('./Json__sql.js'); + +import type {Pg_Client_t as PgTyped_Pg_Client_t} from 'pgtyped-rescript/src/res/PgTyped.gen'; + +import type {t as JSON_t} from './JSON.gen'; + +/** 'Json' parameters type */ +export type jsonParams = void; + +/** 'Json' return type */ +export type jsonResult = { readonly json_object: (undefined | JSON_t) }; + +/** 'Json' query type */ +export type jsonQuery = { readonly params: jsonParams; readonly result: jsonResult }; + +/** Returns an array of all matched results. */ +export const Json_many: (_1:PgTyped_Pg_Client_t, _2:jsonParams) => Promise = Json__sqlJS.Json.many as any; + +/** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ +export const Json_one: (_1:PgTyped_Pg_Client_t, _2:jsonParams) => Promise<(undefined | jsonResult)> = Json__sqlJS.Json.one as any; + +/** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ +export const Json_expectOne: (_1:PgTyped_Pg_Client_t, _2:jsonParams, errorMessage:(undefined | string)) => Promise = Json__sqlJS.Json.expectOne as any; + +/** Executes the query, but ignores whatever is returned by it. */ +export const Json_execute: (_1:PgTyped_Pg_Client_t, _2:jsonParams) => Promise = Json__sqlJS.Json.execute as any; + +export const json: (params:jsonParams, client:PgTyped_Pg_Client_t) => Promise = Json__sqlJS.json as any; + +export const Json: { + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + expectOne: (_1:PgTyped_Pg_Client_t, _2:jsonParams, errorMessage:(undefined | string)) => Promise; + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + one: (_1:PgTyped_Pg_Client_t, _2:jsonParams) => Promise<(undefined | jsonResult)>; + /** Returns an array of all matched results. */ + many: (_1:PgTyped_Pg_Client_t, _2:jsonParams) => Promise; + /** Executes the query, but ignores whatever is returned by it. */ + execute: (_1:PgTyped_Pg_Client_t, _2:jsonParams) => Promise +} = Json__sqlJS.Json as any; diff --git a/packages/example/src/books/Json__sql.res b/packages/example/src/books/Json__sql.res new file mode 100644 index 00000000..3fb77f1f --- /dev/null +++ b/packages/example/src/books/Json__sql.res @@ -0,0 +1,81 @@ +/** Types generated for queries found in "src/books/Json.res" */ +open PgTyped + + +/** 'Json' parameters type */ +@gentype +type jsonParams = unit + +/** 'Json' return type */ +@gentype +type jsonResult = { + json_object: option, +} + +/** 'Json' query type */ +@gentype +type jsonQuery = { + params: jsonParams, + result: jsonResult, +} + +%%private(let jsonIR: IR.t = %raw(`{"usedParamSet":{},"params":[],"statement":"SELECT json_build_object('key', 'value') AS json_object"}`)) + +/** + Runnable query: + ```sql +SELECT json_build_object('key', 'value') AS json_object + ``` + + */ +@gentype +module Json: { + /** Returns an array of all matched results. */ + @gentype + let many: (PgTyped.Pg.Client.t, jsonParams) => promise> + /** Returns exactly 1 result. Returns `None` if more or less than exactly 1 result is returned. */ + @gentype + let one: (PgTyped.Pg.Client.t, jsonParams) => promise> + + /** Returns exactly 1 result. Raises `Exn.t` (with an optionally provided `errorMessage`) if more or less than exactly 1 result is returned. */ + @gentype + let expectOne: ( + PgTyped.Pg.Client.t, + jsonParams, + ~errorMessage: string=? + ) => promise + + /** Executes the query, but ignores whatever is returned by it. */ + @gentype + let execute: (PgTyped.Pg.Client.t, jsonParams) => promise +} = { + @module("pgtyped-rescript-runtime") @new external json: IR.t => PreparedStatement.t = "PreparedQuery"; + let query = json(jsonIR) + let query = (params, ~client) => query->PreparedStatement.run(params, ~client) + + @gentype + let many = (client, params) => query(params, ~client) + + @gentype + let one = async (client, params) => switch await query(params, ~client) { + | [item] => Some(item) + | _ => None + } + + @gentype + let expectOne = async (client, params, ~errorMessage=?) => switch await query(params, ~client) { + | [item] => item + | _ => panic(errorMessage->Option.getOr("More or less than one item was returned")) + } + + @gentype + let execute = async (client, params) => { + let _ = await query(params, ~client) + } +} + +@gentype +@deprecated("Use 'Json.many' directly instead") +let json = (params, ~client) => Json.many(client, params) + + diff --git a/packages/example/src/books/books__sql.gen.tsx b/packages/example/src/books/books__sql.gen.tsx index f9e82435..76f85e62 100644 --- a/packages/example/src/books/books__sql.gen.tsx +++ b/packages/example/src/books/books__sql.gen.tsx @@ -7,8 +7,6 @@ const books__sqlJS = require('./books__sql.js'); import type {Pg_Client_t as PgTyped_Pg_Client_t} from 'pgtyped-rescript/src/res/PgTyped.gen'; -import type {t as JSON_t} from './JSON.gen'; - export type category = "novel" | "science-fiction" | "thriller"; export type iso31661Alpha2 = @@ -235,8 +233,6 @@ export type iso31661Alpha2 = | "TL" | "TM"; -export type arrayJSON_t = JSON_t[]; - export type categoryArray = category[]; export type intArray = number[]; @@ -249,10 +245,8 @@ export type findBookByIdParams = { readonly id?: number }; /** 'FindBookById' return type */ export type findBookByIdResult = { readonly author_id: (undefined | number); - readonly big_int: (undefined | bigint); readonly categories: (undefined | categoryArray); readonly id: number; - readonly meta: (undefined | arrayJSON_t); readonly name: (undefined | string); readonly rank: (undefined | number) }; @@ -266,10 +260,8 @@ export type findBookByCategoryParams = { readonly category?: category }; /** 'FindBookByCategory' return type */ export type findBookByCategoryResult = { readonly author_id: (undefined | number); - readonly big_int: (undefined | bigint); readonly categories: (undefined | categoryArray); readonly id: number; - readonly meta: (undefined | arrayJSON_t); readonly name: (undefined | string); readonly rank: (undefined | number) }; @@ -292,10 +284,8 @@ export type findBookUnicodeParams = void; /** 'FindBookUnicode' return type */ export type findBookUnicodeResult = { readonly author_id: (undefined | number); - readonly big_int: (undefined | bigint); readonly categories: (undefined | categoryArray); readonly id: number; - readonly meta: (undefined | arrayJSON_t); readonly name: (undefined | string); readonly rank: (undefined | number) }; @@ -374,10 +364,8 @@ export type getBooksByAuthorNameParams = { readonly authorName: string }; /** 'GetBooksByAuthorName' return type */ export type getBooksByAuthorNameResult = { readonly author_id: (undefined | number); - readonly big_int: (undefined | bigint); readonly categories: (undefined | categoryArray); readonly id: number; - readonly meta: (undefined | arrayJSON_t); readonly name: (undefined | string); readonly rank: (undefined | number) }; diff --git a/packages/example/src/books/books__sql.res b/packages/example/src/books/books__sql.res index 85036f17..f9947a61 100644 --- a/packages/example/src/books/books__sql.res +++ b/packages/example/src/books/books__sql.res @@ -8,9 +8,6 @@ type category = [#"novel" | #"science-fiction" | #"thriller"] @gentype type iso31661Alpha2 = [#"AD" | #"AE" | #"AF" | #"AG" | #"AI" | #"AL" | #"AM" | #"AO" | #"AQ" | #"AR" | #"AS" | #"AT" | #"AU" | #"AW" | #"AX" | #"AZ" | #"BA" | #"BB" | #"BD" | #"BE" | #"BF" | #"BG" | #"BH" | #"BI" | #"BJ" | #"BL" | #"BM" | #"BN" | #"BO" | #"BQ" | #"BR" | #"BS" | #"BT" | #"BV" | #"BW" | #"BY" | #"BZ" | #"CA" | #"CC" | #"CD" | #"CF" | #"CG" | #"CH" | #"CI" | #"CK" | #"CL" | #"CM" | #"CN" | #"CO" | #"CR" | #"CU" | #"CV" | #"CW" | #"CX" | #"CY" | #"CZ" | #"DE" | #"DJ" | #"DK" | #"DM" | #"DO" | #"DZ" | #"EC" | #"EE" | #"EG" | #"EH" | #"ER" | #"ES" | #"ET" | #"FI" | #"FJ" | #"FK" | #"FM" | #"FO" | #"FR" | #"GA" | #"GB" | #"GD" | #"GE" | #"GF" | #"GG" | #"GH" | #"GI" | #"GL" | #"GM" | #"GN" | #"GP" | #"GQ" | #"GR" | #"GS" | #"GT" | #"GU" | #"GW" | #"GY" | #"HK" | #"HM" | #"HN" | #"HR" | #"HT" | #"HU" | #"ID" | #"IE" | #"IL" | #"IM" | #"IN" | #"IO" | #"IQ" | #"IR" | #"IS" | #"IT" | #"JE" | #"JM" | #"JO" | #"JP" | #"KE" | #"KG" | #"KH" | #"KI" | #"KM" | #"KN" | #"KP" | #"KR" | #"KW" | #"KY" | #"KZ" | #"LA" | #"LB" | #"LC" | #"LI" | #"LK" | #"LR" | #"LS" | #"LT" | #"LU" | #"LV" | #"LY" | #"MA" | #"MC" | #"MD" | #"ME" | #"MF" | #"MG" | #"MH" | #"MK" | #"ML" | #"MM" | #"MN" | #"MO" | #"MP" | #"MQ" | #"MR" | #"MS" | #"MT" | #"MU" | #"MV" | #"MW" | #"MX" | #"MY" | #"MZ" | #"NA" | #"NC" | #"NE" | #"NF" | #"NG" | #"NI" | #"NL" | #"NO" | #"NP" | #"NR" | #"NU" | #"NZ" | #"OM" | #"PA" | #"PE" | #"PF" | #"PG" | #"PH" | #"PK" | #"PL" | #"PM" | #"PN" | #"PR" | #"PS" | #"PT" | #"PW" | #"PY" | #"QA" | #"RE" | #"RO" | #"RS" | #"RU" | #"RW" | #"SA" | #"SB" | #"SC" | #"SD" | #"SE" | #"SG" | #"SH" | #"SI" | #"SJ" | #"SK" | #"SL" | #"SM" | #"SN" | #"SO" | #"SR" | #"SS" | #"ST" | #"SV" | #"SX" | #"SY" | #"SZ" | #"TC" | #"TD" | #"TF" | #"TG" | #"TH" | #"TJ" | #"TK" | #"TL" | #"TM"] -@gentype -type arrayJSON_t = array - @gentype type categoryArray = array @@ -30,10 +27,8 @@ type findBookByIdParams = { @gentype type findBookByIdResult = { author_id: option, - big_int: option, categories: option, id: int, - meta: option, name: option, rank: option, } @@ -115,10 +110,8 @@ type findBookByCategoryParams = { @gentype type findBookByCategoryResult = { author_id: option, - big_int: option, categories: option, id: int, - meta: option, name: option, rank: option, } @@ -281,10 +274,8 @@ type findBookUnicodeParams = unit @gentype type findBookUnicodeResult = { author_id: option, - big_int: option, categories: option, id: int, - meta: option, name: option, rank: option, } @@ -789,10 +780,8 @@ type getBooksByAuthorNameParams = { @gentype type getBooksByAuthorNameResult = { author_id: option, - big_int: option, categories: option, id: int, - meta: option, name: option, rank: option, } diff --git a/packages/query/package.json b/packages/query/package.json index 84ac1ac6..071fac33 100644 --- a/packages/query/package.json +++ b/packages/query/package.json @@ -35,7 +35,8 @@ "pgtyped-rescript-runtime": "^2.2.0", "@pgtyped/wire": "^2.2.0", "chalk": "^4.1.0", - "debug": "^4.1.1" + "debug": "^4.1.1", + "pgsql-ast-parser": "^12.0.1" }, "devDependencies": { "@types/chalk": "^2.2.0", diff --git a/packages/query/src/actions.ts b/packages/query/src/actions.ts index 38af9beb..36c0ac31 100644 --- a/packages/query/src/actions.ts +++ b/packages/query/src/actions.ts @@ -12,6 +12,7 @@ import { createInitialSASLResponse, } from './sasl-helpers.js'; import { DatabaseTypeKind, isEnum, MappableType } from './type.js'; +import { parse, astVisitor, Expr } from 'pgsql-ast-parser'; const debugQuery = debugBase('client:query'); @@ -167,6 +168,7 @@ export interface IQueryTypes { type: MappableType; nullable?: boolean; comment?: string; + checkValues?: ConstraintValue[]; }>; } @@ -327,6 +329,48 @@ export function reduceTypeRows( ); } +async function getCheckConstraints( + fields: TypeField[], + queue: AsyncQueue, +): Promise { + const columnFields = fields.filter((f) => f.columnAttrNumber > 0); + if (columnFields.length === 0) { + return []; + } + + const tableOids = Array.from( + new Set(columnFields.map((f) => f.tableOID)), + ).join(','); + + let rows: string[][] = []; + + try { + rows = await runQuery( + ` + SELECT + conrelid, unnest(conkey) AS attnum, + pg_get_expr(conbin, conrelid) + FROM + pg_constraint + WHERE + contype='c' AND + conrelid IN (${tableOids}) AND + conname NOT LIKE 'pg_%%';`, + queue, + ); + } catch { + // Ignore + } + + return rows + .map(([relid, attnum, def]) => ({ + tableOID: Number(relid), + columnAttrNumber: Number(attnum), + values: parseCheckAllowedValues(def) ?? [], + })) + .filter((r) => r.values.length > 0); +} + // TODO: self-host async function runTypesCatalogQuery( typeOIDs: number[], @@ -360,6 +404,94 @@ OR pt.oid IN (SELECT typelem FROM pg_type ptn WHERE ptn.oid IN (${concatenatedTy ); } +interface ColumnCheck { + tableOID: number; + columnAttrNumber: number; + values: ConstraintValue[]; +} + +export type ConstraintValue = + | { type: 'string'; value: string } + | { type: 'integer'; value: number } + | { type: 'float'; value: number }; + +export function parseCheckAllowedValues(def: string): ConstraintValue[] | null { + try { + // Wrap the constraint expression in a valid SQL statement context + // so the AST parser can parse it properly + const wrappedQuery = `SELECT NULL WHERE ${def}`; + const ast = parse(wrappedQuery); + const values: ConstraintValue[] = []; + let hadInvalidValue = false; + + const visitor = astVisitor((map) => ({ + constant: (node) => { + if (node.type === 'string' && 'value' in node) { + values.push({ type: 'string', value: node.value }); + /* + Can't represent floats as polyvariants in ReScript, so ignore for now. + Unboxed variants would support this though, but we'd need a global schema + file probably. + + } else if (node.type === 'numeric' && 'value' in node) { + values.push({ type: 'float', value: node.value });*/ + } else if (node.type === 'integer' && 'value' in node) { + values.push({ type: 'integer', value: node.value }); + } else { + hadInvalidValue = true; + } + map.super().constant(node); + }, + })); + + const disallowReasons: string[] = []; + + const select = ast[0]; + const where = 'where' in select ? select.where : null; + + if (where != null) { + if ( + where.type === 'binary' && + where.op === '=' && + where.left.type === 'ref' + ) { + // TODO: Match on the ref type so it matches the column name? + const rhs = where.right; + let targetNode: Expr | null = null; + + if (rhs.type === 'call' && rhs.function.name === 'any') { + // Check args + if (rhs.args.length === 1) { + targetNode = rhs.args[0]; + } else { + disallowReasons.push('ANY with multiple args'); + } + } else { + disallowReasons.push('NOT ANY'); + } + + if (targetNode != null) { + visitor.expr(targetNode); + } + } else { + disallowReasons.push('Check constraint too complex'); + } + } + + if (disallowReasons.length > 0) { + /*console.warn('NOT ALLOWED', { + disallowReasons, + wrappedQuery, + });*/ + } + + return values.length > 0 && !hadInvalidValue ? values : null; + } catch { + // Ignore + return null; + } +} + interface ColumnComment { tableOID: number; columnAttrNumber: number; @@ -410,6 +542,7 @@ export async function getTypes( const usedTypesOIDs = paramTypeOIDs.concat(returnTypesOIDs); const typeRows = await runTypesCatalogQuery(usedTypesOIDs, queue); const commentRows = await getComments(fields, queue); + const checkRows = await getCheckConstraints(fields, queue); const typeMap = reduceTypeRows(typeRows); const attrMatcher = ({ @@ -452,10 +585,15 @@ export async function getTypes( for (const c of commentRows) { commentMap[`${c.tableOID}:${c.columnAttrNumber}`] = c.comment; } + const checkMap: { [attid: string]: ConstraintValue[] } = {}; + for (const chk of checkRows) { + checkMap[`${chk.tableOID}:${chk.columnAttrNumber}`] = chk.values; + } const returnTypes = fields.map((f) => ({ ...attrMap[getAttid(f)], ...(commentMap[getAttid(f)] ? { comment: commentMap[getAttid(f)] } : {}), + ...(checkMap[getAttid(f)] ? { checkValues: checkMap[getAttid(f)] } : {}), returnName: f.name, type: typeMap[f.typeOID], }));