diff --git a/package-lock.json b/package-lock.json index 092e9877..34322ab4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,8 +20,9 @@ "prettier": "^2.6.2", "rimraf": "^3.0.2", "semantic-release-plugin-update-version-in-files": "^1.1.0", + "ts-expect": "^1.3.0", "ts-jest": "^28.0.3", - "tsd": "^0.24.1", + "tsd": "^0.31.2", "typedoc": "^0.22.16", "typescript": "~4.7", "wait-for-localhost-cli": "^3.0.0" @@ -1117,10 +1118,14 @@ } }, "node_modules/@tsd/typescript": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-4.8.4.tgz", - "integrity": "sha512-WMFNVstwWGyDuZP2LGPRZ+kPHxZLmhO+2ormstDvnXiyoBPtW1qq9XhhrkI4NVtxgs+2ZiUTl9AG7nNIRq/uCg==", - "dev": true + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-saiCxzHRhUrRxQV2JhH580aQUZiKQUXI38FcAcikcfOomAil4G4lxT0RfrrKywoAYP/rqAdYXYmNRLppcd+hQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17" + } }, "node_modules/@types/babel__core": { "version": "7.1.19", @@ -5478,6 +5483,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ts-expect": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-expect/-/ts-expect-1.3.0.tgz", + "integrity": "sha512-e4g0EJtAjk64xgnFPD6kTBUtpnMVzDrMb12N1YZV0VvSlhnVT3SGxiYTLdGy8Q5cYHOIC/FAHmZ10eGrAguicQ==", + "dev": true, + "license": "MIT" + }, "node_modules/ts-jest": { "version": "28.0.3", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.3.tgz", @@ -5534,14 +5546,16 @@ } }, "node_modules/tsd": { - "version": "0.24.1", - "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.24.1.tgz", - "integrity": "sha512-sD+s81/2aM4RRhimCDttd4xpBNbUFWnoMSHk/o8kC8Ek23jljeRNWjsxFJmOmYLuLTN9swRt1b6iXfUXTcTiIA==", + "version": "0.31.2", + "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.31.2.tgz", + "integrity": "sha512-VplBAQwvYrHzVihtzXiUVXu5bGcr7uH1juQZ1lmKgkuGNGT+FechUCqmx9/zk7wibcqR2xaNEwCkDyKh+VVZnQ==", "dev": true, + "license": "MIT", "dependencies": { - "@tsd/typescript": "~4.8.3", + "@tsd/typescript": "~5.4.3", "eslint-formatter-pretty": "^4.1.0", "globby": "^11.0.1", + "jest-diff": "^29.0.3", "meow": "^9.0.0", "path-exists": "^4.0.0", "read-pkg-up": "^7.0.0" @@ -5553,6 +5567,39 @@ "node": ">=14.16" } }, + "node_modules/tsd/node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/tsd/node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tsd/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/tsd/node_modules/camelcase-keys": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", @@ -5579,6 +5626,16 @@ "node": ">=0.10.0" } }, + "node_modules/tsd/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/tsd/node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -5600,6 +5657,32 @@ "node": ">=8" } }, + "node_modules/tsd/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/tsd/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/tsd/node_modules/meow": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", @@ -5641,6 +5724,21 @@ "node": ">=10" } }, + "node_modules/tsd/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/tsd/node_modules/quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -5650,6 +5748,13 @@ "node": ">=8" } }, + "node_modules/tsd/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/tsd/node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -6938,9 +7043,9 @@ } }, "@tsd/typescript": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-4.8.4.tgz", - "integrity": "sha512-WMFNVstwWGyDuZP2LGPRZ+kPHxZLmhO+2ormstDvnXiyoBPtW1qq9XhhrkI4NVtxgs+2ZiUTl9AG7nNIRq/uCg==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-saiCxzHRhUrRxQV2JhH580aQUZiKQUXI38FcAcikcfOomAil4G4lxT0RfrrKywoAYP/rqAdYXYmNRLppcd+hQQ==", "dev": true }, "@types/babel__core": { @@ -10158,6 +10263,12 @@ "integrity": "sha512-GJtWyq9InR/2HRiLZgpIKv+ufIKrVrvjQWEj7PxAXNc5dwbNJkqhAUoAGgzRmULAnoOM5EIpveYd3J2VeSAIew==", "dev": true }, + "ts-expect": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-expect/-/ts-expect-1.3.0.tgz", + "integrity": "sha512-e4g0EJtAjk64xgnFPD6kTBUtpnMVzDrMb12N1YZV0VvSlhnVT3SGxiYTLdGy8Q5cYHOIC/FAHmZ10eGrAguicQ==", + "dev": true + }, "ts-jest": { "version": "28.0.3", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.3.tgz", @@ -10183,19 +10294,41 @@ } }, "tsd": { - "version": "0.24.1", - "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.24.1.tgz", - "integrity": "sha512-sD+s81/2aM4RRhimCDttd4xpBNbUFWnoMSHk/o8kC8Ek23jljeRNWjsxFJmOmYLuLTN9swRt1b6iXfUXTcTiIA==", + "version": "0.31.2", + "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.31.2.tgz", + "integrity": "sha512-VplBAQwvYrHzVihtzXiUVXu5bGcr7uH1juQZ1lmKgkuGNGT+FechUCqmx9/zk7wibcqR2xaNEwCkDyKh+VVZnQ==", "dev": true, "requires": { - "@tsd/typescript": "~4.8.3", + "@tsd/typescript": "~5.4.3", "eslint-formatter-pretty": "^4.1.0", "globby": "^11.0.1", + "jest-diff": "^29.0.3", "meow": "^9.0.0", "path-exists": "^4.0.0", "read-pkg-up": "^7.0.0" }, "dependencies": { + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, "camelcase-keys": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", @@ -10213,6 +10346,12 @@ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, "hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -10228,6 +10367,24 @@ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, "meow": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", @@ -10260,12 +10417,29 @@ "validate-npm-package-license": "^3.0.1" } }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, "quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", diff --git a/package.json b/package.json index fc30dd3e..61e91cca 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,9 @@ "prettier": "^2.6.2", "rimraf": "^3.0.2", "semantic-release-plugin-update-version-in-files": "^1.1.0", + "ts-expect": "^1.3.0", "ts-jest": "^28.0.3", - "tsd": "^0.24.1", + "tsd": "^0.31.2", "typedoc": "^0.22.16", "typescript": "~4.7", "wait-for-localhost-cli": "^3.0.0" diff --git a/src/select-query-parser.ts b/src/select-query-parser.ts index 0c5c40f1..0461a821 100644 --- a/src/select-query-parser.ts +++ b/src/select-query-parser.ts @@ -1,7 +1,7 @@ // Credits to @bnjmnt4n (https://www.npmjs.com/package/postgrest-query) // See https://github.com/PostgREST/postgrest/blob/2f91853cb1de18944a4556df09e52450b881cfb3/src/PostgREST/ApiRequest/QueryParams.hs#L282-L284 -import { GenericSchema, Prettify } from './types' +import { GenericRelationship, GenericSchema, Prettify } from './types' type Whitespace = ' ' | '\n' | '\t' @@ -101,6 +101,8 @@ type AggregateFunctions = 'count' | 'sum' | 'avg' | 'min' | 'max' type StripUnderscore = T extends `_${infer U}` ? U : T +type ContainsNull = null extends T ? true : false; + type TypeScriptTypes = T extends ArrayPostgreSQLTypes ? TypeScriptSingleValueTypes>>[] : TypeScriptSingleValueTypes @@ -141,6 +143,19 @@ type HasFKey = Relationships extends [infer R] : HasFKey : false +/** + * Returns a boolean representing whether there is a foreign key with the given columns. + */ +type HasFKeyMatchingColumn = Relationships extends [infer R] + ? R extends { columns: Columns } + ? true + : false + : Relationships extends [infer R, ...infer Rest] + ? HasFKeyMatchingColumn extends true + ? true + : HasFKeyMatchingColumn + : false + /** * Returns a boolean representing whether there the foreign key has a unique constraint. */ @@ -178,6 +193,294 @@ type HasUniqueFKeyToFRel = Relationships extends [infer : HasUniqueFKeyToFRel : false +/** + * Checks if there is more than one relation to a given foreign relation name in the Relationships. + */ +type HasMultipleFKeysToFRel = Relationships extends [ + infer R, + ...infer Rest +] + ? R extends { referencedRelation: FRelName } + ? HasFKeyToFRel extends true + ? true + : HasMultipleFKeysToFRel + : HasMultipleFKeysToFRel + : false + +type TablesAndViews = Schema['Tables'] & Exclude + +type RequireHintingSelectQueryError< + Field extends { original: string }, + RelationName +> = SelectQueryError<`Could not embed because more than one relationship was found for '${Field['original']}' and '${RelationName extends string + ? RelationName + : 'unkown'}' you need to hint the column with ${Field['original']}! ?`> + +type IsEmptyOrUnkownObject = T extends Record + ? true + : T extends unknown + ? keyof T extends never + ? true + : false + : false; + +type FindMatchingRelationships = Relationships extends [infer R, ...infer Rest] + ? R extends { foreignKeyName: value } + ? R + : R extends { referencedRelation: value } + ? R + : R extends { columns: [value] } + ? R + : FindMatchingRelationships + : never + +type FindFieldMathingRelationships = FindMatchingRelationships + +// Given a Schema, will get a smaller schema where all the relationships referencedRelation will have to match +// one of the key of Schema['Tables'] and return the new schema where all the relationships of all the Tables will +// always only point to other tables +type GetSchemaWithoutViewsRelationships = { + [TableName in keyof Schema['Tables']]: { + Row: Schema['Tables'][TableName]['Row']; + Relationships: Schema['Tables'][TableName] extends { Relationships: infer R } + ? { + [K in keyof R]: R[K] extends { referencedRelation: infer RefRel } + ? RefRel extends keyof Schema['Tables'] + ? R[K] + : never + : never + } + : never + } +} + +type FindTableFromFkeyName = +{ +[TableName in keyof Schema['Tables']]: Schema['Tables'][TableName] extends { Relationships: infer R } + ? R extends Array<{ foreignKeyName: string }> + ? FKName extends R[number]['foreignKeyName'] + ? Schema['Tables'][TableName] + : never + : never + : never +}[keyof Schema['Tables']] extends infer TableResult + ? TableResult extends never + ? { + [ViewName in keyof Schema['Views']]: Schema['Views'][ViewName] extends { Relationships: infer R } + ? R extends Array<{ foreignKeyName: string }> + ? FKName extends R[number]['foreignKeyName'] + ? Schema['Views'][ViewName] + : never + : never + : never + }[keyof Schema['Views']] + : TableResult + : never + +type FindReferencedTableFromFkeyName = { + [TableName in keyof Schema['Tables']]: Schema['Tables'][TableName] extends { Relationships: infer R } + ? R extends Array<{ foreignKeyName: string; referencedRelation: string }> + ? FKName extends R[number]['foreignKeyName'] + ? Schema['Tables'][Extract['referencedRelation']] + : never + : never + : never +}[keyof Schema['Tables']] extends infer Result + ? Result extends never + ? never + : Result + : never + + +type FindReferencedViewFromFkeyName = { + [ViewName in keyof Schema['Views']]: Schema['Views'][ViewName] extends { Relationships: infer R } + ? R extends Array<{ foreignKeyName: string; referencedRelation: string }> + ? FKName extends R[number]['foreignKeyName'] + ? Schema['Views'][Extract['referencedRelation']] + : never + : never + : never + }[keyof Schema['Views']] extends infer Result + ? Result extends never + ? never + : Result + : never + +type FindReferencedRowFromFkeyName = + FindReferencedTableFromFkeyName extends { Row: Record } + ? FindReferencedTableFromFkeyName['Row'] + : FindReferencedViewFromFkeyName extends { Row: Record } + ? FindReferencedViewFromFkeyName['Row'] + : never + + + type GetRelationFKName = + Schema['Tables'][ReferencedName] extends { Relationships: infer R } + ? R extends GenericRelationship[] + ? GetRelationFKNameHelper + : never + : never + +type GetRelationFKNameHelper< +Schema extends GenericSchema, +R extends GenericRelationship[], +FKName extends string +> = R extends [infer First, ...infer Rest] +// We exclude all the relations pointing to views +? First extends { referencedRelation: keyof Schema['Tables'] } + ? First extends { foreignKeyName: FKName } + ? First + : GetRelationFKNameHelper + : GetRelationFKNameHelper +: never + + +// Returns true if the Needle match one of the Tables or Views from the schema +type IsRelationFKName = +GetRelationFKName extends never ? false : true; + + +type GetRelationReferenceByColumnName< + Schema extends GenericSchema, + ReferencedName extends string, + Columns extends string[] +> = Schema['Tables'][ReferencedName] extends { Relationships: infer R } + ? R extends GenericRelationship[] + ? GetRelationReferenceByColumnNameHelper + : never + : never + +type GetRelationReferenceByColumnNameHelper< + Schema extends GenericSchema, + R extends GenericRelationship[], + Columns extends string[] +> = R extends [infer First, ...infer Rest] + // We exclude all the relations pointing to views + ? First extends { referencedRelation: keyof Schema['Tables'] } + ? First extends { columns: Columns } + ? First + : GetRelationReferenceByColumnNameHelper + : GetRelationReferenceByColumnNameHelper + : never + +type IsRelationReferenceByColumnName< + Schema extends GenericSchema, + ReferencedName extends string, + Columns extends string[] +> = GetRelationReferenceByColumnName extends never ? false : true + +type GetRelationReferencedName< + Schema extends GenericSchema, + ReferenceName extends string, + ReferencedName extends string +> = TablesAndViews[ReferenceName] extends { Relationships: GenericRelationship[] } + ? GetRelationReferencedNameHelper[ReferenceName]['Relationships'], ReferencedName> + : never + +type GetRelationReferencedNameHelper< + Schema extends GenericSchema, + Relationships extends GenericRelationship[], + ReferencedName extends string +> = Relationships extends [infer First, ...infer Rest] + ? First extends { referencedRelation: ReferencedName } + ? First + : GetRelationReferencedNameHelper + : never + +type IsRelationReferencedName< + Schema extends GenericSchema, + ReferenceName extends string, + ReferencedName extends string +> = GetRelationReferencedName extends never ? false : true + + +type FindMathingRelationship = + GetRelationFKName extends never + ? GetRelationReferencedName extends never + ? GetRelationReferenceByColumnName extends never + ? never + : { relationship: GetRelationReferenceByColumnName, type: 'colname' } + : { relationship: GetRelationReferencedName, type: 'refname' } + : { relationship: GetRelationFKName, type: 'fkname' } + +type FindMatchingRelationshipRow = + FindMathingRelationship extends never + ? never + : TablesAndViews[FindMathingRelationship['relationship']['referencedRelation']] extends { Row: Record } + ? TablesAndViews[FindMathingRelationship['relationship']['referencedRelation']]['Row'] + : never + + +type FindMatchingRelationshipTable = + FindMathingRelationship extends never + ? never + : TablesAndViews[FindMathingRelationship['relationship']['referencedRelation']] extends { Row: Record } + ? TablesAndViews[FindMathingRelationship['relationship']['referencedRelation']] + : never + + + type FindOriginTableFromFkeyName = { + [TableName in keyof Schema['Tables']]: Schema['Tables'][TableName] extends { Relationships: infer R } + ? R extends Array<{ foreignKeyName: string; referencedRelation: string }> + ? FKName extends R[number]['foreignKeyName'] + ? Schema['Tables'][TableName] + : never + : never + : never + }[keyof Schema['Tables']] extends infer Result + ? Result extends never + ? never + : Result + : never + + + type FindOriginViewFromFkeyName = { + [ViewName in keyof Schema['Views']]: Schema['Views'][ViewName] extends { Relationships: infer R } + ? R extends Array<{ foreignKeyName: string; referencedRelation: string }> + ? FKName extends R[number]['foreignKeyName'] + ? Schema['Views'][ViewName] + : never + : never + : never + }[keyof Schema['Views']] extends infer Result + ? Result extends never + ? never + : Result + : never + + type FindOriginRowFromFkeyName = + FindOriginTableFromFkeyName extends { Row: Record } + ? FindOriginTableFromFkeyName['Row'] + : FindOriginViewFromFkeyName extends { Row: Record } + ? FindOriginViewFromFkeyName['Row'] + : never + + +type IsColumnsNullable }, Columns extends (keyof Table['Row'])[]> = + Columns extends [infer Column, ...infer Rest] + ? Column extends keyof Table['Row'] + ? ContainsNull extends true + ? true + : IsColumnsNullable + : never + : false + +type IsRelationNullable = + FindTableFromFkeyName extends infer Table + ? Table extends { Row: Record, Relationships: unknown[] } + ? FindMatchingRelationships extends { columns: (keyof Table['Row'])[] } + ? IsColumnsNullable['columns']> + : never + : never + : never + + +type SchemaWithInferedRelationships = TablesAndViews[Field['original']] extends { + Relationships: infer R +} + ? R + : unknown + /** * Constructs a type definition for a single field of an object. * @@ -192,100 +495,250 @@ type ConstructFieldDefinition< RelationName, Relationships, Field -> = Field extends { star: true } +> = IsEmptyOrUnkownObject extends true + // If our row is an empty or unknown object, we failed to properly infer the + // type of our row, this happen in case of implicit aliasing via columns, fkNames or referenced tables alias + // foreignKeyName(*) or relationColumnName(*) or referencedTable(*) + // --------------------------- + // Check if our RelationName match one of our ForeignKeyName + ? RelationName extends string + // If it does, use the foreignKey referenced table row as the new Row type + ? IsForeignKeyName extends true + ? ConstructFieldDefinition< + Schema, + FindOriginRowFromFkeyName, + RelationName, + Relationships, + Field + > + : {f: Field, rn: RelationName, rs: Relationships, r: Row, infwithrel: SchemaWithInferedRelationships} + : 'relation-name-is-not-a-string' + : Field extends { star: true } ? Row : Field extends { spread: true; original: string; children: unknown[] } ? GetResultHelper< Schema, - (Schema['Tables'] & Schema['Views'])[Field['original']]['Row'], + TablesAndViews[Field['original']]['Row'], Field['original'], - (Schema['Tables'] & Schema['Views'])[Field['original']] extends { Relationships: infer R } - ? R - : unknown, - Field['children'], + SchemaWithInferedRelationships, + Field['children'] & {'branch-unkown-field': true}, unknown > : Field extends { children: [] } ? {} - : Field extends { name: string; original: string; hint: string; children: unknown[] } + : Field extends { name: string; original: string; hint?: string; children: unknown[] } ? { - [_ in Field['name']]: GetResultHelper< + [_ in Field['name']]: ConstructFieldWithChildren< Schema, - (Schema['Tables'] & Schema['Views'])[Field['original']]['Row'], - Field['original'], - (Schema['Tables'] & Schema['Views'])[Field['original']] extends { Relationships: infer R } - ? R - : unknown, - Field['children'], - unknown - > extends infer Child - ? // One-to-one relationship - referencing column(s) has unique/pkey constraint. - HasUniqueFKey< - Field['hint'], - (Schema['Tables'] & Schema['Views'])[Field['original']] extends { - Relationships: infer R - } - ? R - : unknown - > extends true - ? Field extends { inner: true } - ? Child - : Child | null - : Relationships extends unknown[] - ? HasFKey extends true - ? Field extends { inner: true } - ? Child - : Child | null - : Child[] - : Child[] - : never - } - : Field extends { name: string; original: string; children: unknown[] } - ? { - [_ in Field['name']]: GetResultHelper< - Schema, - (Schema['Tables'] & Schema['Views'])[Field['original']]['Row'], - Field['original'], - (Schema['Tables'] & Schema['Views'])[Field['original']] extends { Relationships: infer R } - ? R - : unknown, - Field['children'], - unknown - > extends infer Child - ? // One-to-one relationship - referencing column(s) has unique/pkey constraint. - HasUniqueFKeyToFRel< - RelationName, - (Schema['Tables'] & Schema['Views'])[Field['original']] extends { - Relationships: infer R - } - ? R - : unknown - > extends true - ? Field extends { inner: true } - ? Child - : Child | null - : Relationships extends unknown[] - ? HasFKeyToFRel extends true - ? Field extends { inner: true } - ? Child - : Field extends { left: true } - ? // TODO: This should return null only if the column is actually nullable - Child | null - : Child | null - : Child[] - : Child[] - : never + Field & { 'branch-schema-infered-field': true }, + RelationName, + Relationships, + SchemaWithInferedRelationships + > } : Field extends { name: string; type: infer T } ? { [K in Field['name']]: T } : Field extends { name: string; original: string } - ? Field['original'] extends keyof Row - ? { [K in Field['name']]: Row[Field['original']] } - : Field['original'] extends 'count' - ? { count: number } - : SelectQueryError<`Referencing missing column \`${Field['original']}\``> + ? ConstructSimpleField : Record +type ConstructFieldWithChildren< + Schema extends GenericSchema, + Field extends { name: string; original: string; hint?: string; children: unknown[]; inner?: boolean; left?: boolean }, + RelationName, + Relationships, + InferedRelationships +> = GetResultHelper< + Schema, + TablesAndViews[Field['original']]['Row'], + Field['original'], + InferedRelationships, + Field['children'], + unknown +> extends infer Child + ? 'hint' extends keyof Field + ? ConstructFieldWithHint + : ConstructFieldWithoutHint + : never + +type ConstructFieldWithHint< + Schema extends GenericSchema, + Field extends { original: string; hint: string; inner?: boolean }, + Relationships, + Child +> = HasUniqueFKey> extends true + ? Field extends { inner: true } + ? Child + : Child | null + : Relationships extends unknown[] + ? HasFKey extends true + ? Field extends { inner: true } + ? Child + : FindMatchingRelationships extends { foreignKeyName: string } + ? IsRelationNullable['foreignKeyName']> extends true + ? Child | null + : Child + : Child | null + : HasFKeyMatchingColumn<[Field['hint']], Relationships> extends true + ? FindMatchingRelationships extends { foreignKeyName: string } + ? IsRelationNullable['foreignKeyName']> extends true + ? Child | null + : Child + : Child | null + : // .from('users').select(`first_friend_of:best_friends!best_friends_first_user_fkey(*),second_friend_of:best_friends!best_friends_second_user_fkey(*),third_wheel_of:best_friends!best_friends_third_wheel_fkey(*)`) + Child[] + : never + +type ConstructFieldWithoutHint< + Schema extends GenericSchema, + Field extends { original: string; inner?: boolean; left?: boolean }, + RelationName, + Relationships, + InferedRelationships, + Child +> = HasUniqueFKeyToFRel extends true + ? Field extends { inner: true } + ? Child + : Child | null + : Relationships extends unknown[] + ? ConstructFieldWithRelationships + : never + +type ConstructFieldWithRelationships< + Schema extends GenericSchema, + Field extends { original: string; inner?: boolean; left?: boolean }, + RelationName, + Relationships, + Child +> = HasFKeyToFRel extends true + ? ConstructFieldWithFKeyToFRel + : ConstructFieldWithoutFKeyToFRel + +type ConstructFieldWithFKeyToFRel< + Schema extends GenericSchema, + Field extends { original: string; inner?: boolean; left?: boolean }, + RelationName, + Relationships, + Child +> = Field extends { inner: true } + ? Child + : Field extends { left: true } + ? HasMultipleFKeysToFRel extends true + ? RequireHintingSelectQueryError + : FindFieldMathingRelationships extends { foreignKeyName: string } + ? IsRelationNullable['foreignKeyName']> extends true + ? Child | null + : Child + : Child | null + : HasMultipleFKeysToFRel extends true + ? RequireHintingSelectQueryError + : FindFieldMathingRelationships extends { foreignKeyName: string } + ? IsRelationNullable['foreignKeyName']> extends true + ? Child | null + : Child + : Child | null + +type ConstructFieldWithoutFKeyToFRel< + Schema extends GenericSchema, + Field extends { original: string }, + RelationName, + Relationships, + Child +> = HasMultipleFKeysToFRel< + RelationName, + SchemaWithInferedRelationships +> extends true + ? // For 1-M relationships + RequireHintingSelectQueryError + : Child extends unknown + ? ConstructFieldWithUnknownChild + : never + +type IsForeignKeyName< + Schema extends GenericSchema, + FKName extends string +> = { + [TableName in keyof Schema['Tables']]: Schema['Tables'][TableName] extends { Relationships: infer R } + ? R extends Array<{ foreignKeyName: string }> + ? FKName extends R[number]['foreignKeyName'] + ? true + : never + : never + : never +}[keyof Schema['Tables']] extends never + ? false + : true + +type ConstructFieldWithUnknownChild< + Schema extends GenericSchema, + Field extends { original: string }, + RelationName, + Relationships, + Child +> = + // Our Child cannot be infered by previous logic, it's an alias such as + // reference via a relation table alias + // reference via a column holding relation alias + IsEmptyOrUnkownObject extends true + // Check if the RelationName is a direct ForeignKey reference + ? RelationName extends string + ? IsForeignKeyName extends true + ? {type: 'relation-name-is-fk-name', + f: Field, + isFkName: IsForeignKeyName + rname: RelationName, g: FindTableFromFkeyName } + : IsForeignKeyName extends true + ? 'field-original-is-fk-name' + : 'idk' + : Child[] + : Child[] + // // Try to find relations matching our RelationName + // ? GetAllReferencedRelations extends [] + // // Check if our Field['original'] match one of our relationship + // // either by direct fkname or via column matching or table reference + // ? FindMatchingRelationships> extends {foreignKeyName: string} + // ? FindTableFromFkeyName extends { Row: Record } + // ? GetResultHelper< + // Schema, + // FindTableFromFkeyName['Row'], + // Field['original'], + // SchemaWithInferedRelationships, + // Field extends {'children': unknown[]} ? Field['children'] : [], + // unknown + // >[] + // : Child[] + // : Child[] + // // If no relations were found referencing RelationName the reference might be + // // aliased via Columns name + // : FindMatchingRelationships extends { foreignKeyName: string } + // // We found a matching relationship, we build our result from the related table + // ? FindReferencedTableFromFkeyName['foreignKeyName']> extends { Row: Record } + // ? GetResultHelper< + // Schema, + // FindReferencedTableFromFkeyName['foreignKeyName']>['Row'], + // Field['original'], + // SchemaWithInferedRelationships, + // Field extends {'children': unknown[]} ? Field['children'] : [], + // unknown + // >[] + // : { rr: FindReferencedTableFromFkeyName['foreignKeyName']> } + // : 'toto' + + // : Child[] + // : Child[] + +type ConstructSimpleField< + Row extends Record, + Field extends { name: string; original: string } +> = Field['original'] extends keyof Row + ? { [K in Field['name']]: Row[Field['original']] } + : Field['name'] extends keyof Row + ? { [K in Field['name']]: Row[K] } + : Field['original'] extends AggregateFunctions + ? { [K in Field['name']]: number } + : { r: Row, F: Field, sq: SelectQueryError<`Referencing missing column \`${Field['original']}\``>} + + /** * Notes: all `Parse*` types assume that their input strings have their whitespace * removed. They return tuples of ["Return Value", "Remainder of text"] or @@ -363,7 +816,16 @@ type ParseField = Input extends '' ? EatWhitespace extends `!inner${infer Remainder}` ? ParseEmbeddedResource> extends [infer Fields, `${infer Remainder}`] ? // `field!inner(nodes)` - [{ name: Name; original: Name; children: Fields; inner: true }, EatWhitespace] + [ + { + name: Name + original: Name + children: Fields + inner: true + parseFieldBranch: 'field!inner(nodes)' + }, + EatWhitespace + ] : CreateParserErrorIfRequired< ParseEmbeddedResource>, 'Expected embedded resource after `!inner`' @@ -371,7 +833,16 @@ type ParseField = Input extends '' : EatWhitespace extends `!left${infer Remainder}` ? ParseEmbeddedResource> extends [infer Fields, `${infer Remainder}`] ? // `field!left(nodes)` - [{ name: Name; original: Name; children: Fields; left: true }, EatWhitespace] + [ + { + name: Name + original: Name + children: Fields + left: true + parseFieldBranch: 'field!left(nodes)' + }, + EatWhitespace + ] : CreateParserErrorIfRequired< ParseEmbeddedResource>, 'Expected embedded resource after `!left`' @@ -385,7 +856,14 @@ type ParseField = Input extends '' ] ? // `field!hint!inner(nodes)` [ - { name: Name; original: Name; hint: Hint; children: Fields; inner: true }, + { + name: Name + original: Name + hint: Hint + children: Fields + inner: true + parseFieldBranch: 'field!hint!inner(nodes)' + }, EatWhitespace ] : CreateParserErrorIfRequired< @@ -397,7 +875,16 @@ type ParseField = Input extends '' `${infer Remainder}` ] ? // `field!hint(nodes)` - [{ name: Name; original: Name; hint: Hint; children: Fields }, EatWhitespace] + [ + { + name: Name + original: Name + hint: Hint + children: Fields + parseFieldBranch: 'field!hint(nodes)' + }, + EatWhitespace + ] : CreateParserErrorIfRequired< ParseEmbeddedResource>, 'Expected embedded resource after `!hint`' @@ -405,7 +892,10 @@ type ParseField = Input extends '' : ParserError<'Expected identifier after `!`'> : ParseEmbeddedResource> extends [infer Fields, `${infer Remainder}`] ? // `field(nodes)` - [{ name: Name; original: Name; children: Fields }, EatWhitespace] + [ + { name: Name; original: Name; children: Fields; parseFieldBranch: 'field(nodes)' }, + EatWhitespace + ] : ParseEmbeddedResource> extends ParserError ? // Return error if start of embedded resource was detected but not found. ParseEmbeddedResource> @@ -442,6 +932,7 @@ type ParseFieldWithoutEmbeddedResource = name: AggregateFunction original: AggregateFunction type: Type + parseFieldWithoutEmbeddedResourceBranch: 'field.aggregate()::type' }, EatWhitespace ] @@ -452,13 +943,14 @@ type ParseFieldWithoutEmbeddedResource = Omit & { name: AggregateFunction original: AggregateFunction + parseFieldWithoutEmbeddedResourceBranch: 'field.aggregate()' }, EatWhitespace ] : ParseFieldAggregation> extends ParserError ? ParseFieldAggregation> : // `field` - [Field, EatWhitespace] + [Field & { parseFieldWithoutEmbeddedResourceBranch: 'field' }, EatWhitespace] : CreateParserErrorIfRequired< ParseFieldWithoutEmbeddedResourceAndAggregation, `Expected field at \`${Input}\`` @@ -476,11 +968,22 @@ type ParseFieldWithoutEmbeddedResourceAndAggregation = ParseFieldWithoutEmbeddedResourceAndTypeCast extends [infer Field, `${infer Remainder}`] ? ParseFieldTypeCast> extends [infer Type, `${infer Remainder}`] ? // `field::type` or `field->json...::type` - [Omit & { type: Type }, EatWhitespace] + [ + Omit & { + type: Type + parseFieldWithoutEmbeddedResourceAndAggregationBranch: 'field::type or field->json...::type' + }, + EatWhitespace + ] : ParseFieldTypeCast> extends ParserError ? ParseFieldTypeCast> : // `field` or `field->json...` - [Field, EatWhitespace] + [ + Field & { + parseFieldWithoutEmbeddedResourceAndAggregationBranch: 'field or field->json...' + }, + EatWhitespace + ] : CreateParserErrorIfRequired< ParseFieldWithoutEmbeddedResourceAndTypeCast, `Expected field at \`${Input}\`` @@ -501,11 +1004,23 @@ type ParseFieldWithoutEmbeddedResourceAndTypeCast = ] ? // `field->json...` [ - { name: PropertyName; original: PropertyName; type: PropertyType }, + { + name: PropertyName + original: PropertyName + type: PropertyType + parseFieldWithoutEmbeddedResourceAndTypeCastBranch: 'field->json...' + }, EatWhitespace ] : // `field` - [{ name: Name; original: Name }, EatWhitespace] + [ + { + name: Name + original: Name + parseFieldWithoutEmbeddedResourceAndTypeCastBranch: 'field' + }, + EatWhitespace + ] : ParserError<`Expected field at \`${Input}\``> /** @@ -516,7 +1031,10 @@ type ParseFieldTypeCast = EatWhitespace extends `:: ? ParseIdentifier> extends [`${infer CastType}`, `${infer Remainder}`] ? // Ensure that `CastType` is a valid type. CastType extends PostgreSQLTypes - ? [TypeScriptTypes, EatWhitespace] + ? [ + TypeScriptTypes, + EatWhitespace + ] : ParserError<`Invalid type for \`::\` operator \`${CastType}\``> : ParserError<`Invalid type for \`::\` operator at \`${Remainder}\``> : Input @@ -557,7 +1075,10 @@ type ParseNode = Input extends '' Input extends `...${infer Remainder}` ? ParseField> extends [infer Field, `${infer Remainder}`] ? Field extends { children: unknown[] } - ? [Prettify<{ spread: true } & Field>, EatWhitespace] + ? [ + Prettify<{ spread: true } & Field> & { parseNodeBranch: '...field' }, + EatWhitespace + ] : ParserError<'Unable to parse spread resource'> : ParserError<'Unable to parse spread resource'> : ParseIdentifier extends [infer Name, `${infer Remainder}`] @@ -569,11 +1090,14 @@ type ParseNode = Input extends '' ? // `renamed_field:` ParseField> extends [infer Field, `${infer Remainder}`] ? Field extends { name: string } - ? [Prettify & { name: Name }>, EatWhitespace] + ? [ + Prettify & { name: Name; parseNodeBranch: 'renamed_field:field' }>, + EatWhitespace + ] : ParserError<`Unable to parse renamed field`> : ParserError<`Unable to parse renamed field`> : // Otherwise, just parse it as a field without renaming. - ParseField + ParseField & { parseNodeBranch: 'field' } : ParserError<`Expected identifier at \`${Input}\``> /** @@ -618,7 +1142,6 @@ type ParseEmbeddedResource = Input extends `(${infer Remai : ParseNodes> : ParserError<'Expected embedded resource fields or `)`'> : Input - /** * Parses a sequence of nodes, separated by `,`. * diff --git a/src/types.ts b/src/types.ts index 5379b271..704d306d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -39,20 +39,31 @@ export type PostgrestSingleResponse = PostgrestResponseSuccess | Postgrest export type PostgrestMaybeSingleResponse = PostgrestSingleResponse export type PostgrestResponse = PostgrestSingleResponse +export type GenericRelationship = { + foreignKeyName: string + columns: string[] + isOneToOne: boolean + referencedRelation: string + referencedColumns: string[] +} + export type GenericTable = { Row: Record Insert: Record Update: Record + Relationships: GenericRelationship[] } export type GenericUpdatableView = { Row: Record Insert: Record Update: Record + Relationships: GenericRelationship[] } export type GenericNonUpdatableView = { Row: Record + Relationships: GenericRelationship[] } export type GenericView = GenericUpdatableView | GenericNonUpdatableView diff --git a/test/basic.ts b/test/basic.ts index 6b46d50f..13ff8eb8 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -268,8 +268,8 @@ test('on_conflict insert', async () => { }, ], "error": null, - "status": 201, - "statusText": "Created", + "status": 200, + "statusText": "OK", } `) }) @@ -366,8 +366,8 @@ describe('basic insert, update, delete', () => { }, ], "error": null, - "status": 201, - "statusText": "Created", + "status": 200, + "statusText": "OK", } `) @@ -1481,239 +1481,3 @@ test('update with no match - return=representation', async () => { } `) }) - -test('!left join on one to one relation', async () => { - const res = await postgrest.from('channel_details').select('channels!left(id)').limit(1).single() - expect(Array.isArray(res.data?.channels)).toBe(false) - // TODO: This should not be nullable - expect(res.data?.channels?.id).not.toBeNull() - expect(res).toMatchInlineSnapshot(` - Object { - "count": null, - "data": Object { - "channels": Object { - "id": 1, - }, - }, - "error": null, - "status": 200, - "statusText": "OK", - } - `) -}) - -test('!left join on one to many relation', async () => { - const res = await postgrest.from('users').select('messages!left(username)').limit(1).single() - expect(Array.isArray(res.data?.messages)).toBe(true) - expect(res).toMatchInlineSnapshot(` - Object { - "count": null, - "data": Object { - "messages": Array [ - Object { - "username": "supabot", - }, - Object { - "username": "supabot", - }, - ], - }, - "error": null, - "status": 200, - "statusText": "OK", - } - `) -}) - -test('!left join on one to 0-1 non-empty relation', async () => { - const res = await postgrest - .from('users') - .select('user_profiles!left(username)') - .eq('username', 'supabot') - .limit(1) - .single() - expect(Array.isArray(res.data?.user_profiles)).toBe(true) - expect(res.data?.user_profiles[0].username).not.toBeNull() - expect(res).toMatchInlineSnapshot(` - Object { - "count": null, - "data": Object { - "user_profiles": Array [ - Object { - "username": "supabot", - }, - ], - }, - "error": null, - "status": 200, - "statusText": "OK", - } - `) -}) - -test('!left join on zero to one with null relation', async () => { - const res = await postgrest - .from('user_profiles') - .select('*,users!left(*)') - .eq('id', 2) - .limit(1) - .single() - expect(Array.isArray(res.data?.users)).toBe(false) - expect(res.data?.users).toBeNull() - - expect(res).toMatchInlineSnapshot(` - Object { - "count": null, - "data": Object { - "id": 2, - "username": null, - "users": null, - }, - "error": null, - "status": 200, - "statusText": "OK", - } - `) -}) - -test('!left join on zero to one with valid relation', async () => { - const res = await postgrest - .from('user_profiles') - .select('*,users!left(status)') - .eq('id', 1) - .limit(1) - .single() - expect(Array.isArray(res.data?.users)).toBe(false) - // TODO: This should be nullable indeed - expect(res.data?.users?.status).not.toBeNull() - - expect(res).toMatchInlineSnapshot(` - Object { - "count": null, - "data": Object { - "id": 1, - "username": "supabot", - "users": Object { - "status": "ONLINE", - }, - }, - "error": null, - "status": 200, - "statusText": "OK", - } - `) -}) - -test('!left join on zero to one empty relation', async () => { - const res = await postgrest - .from('users') - .select('user_profiles!left(username)') - .eq('username', 'dragarcia') - .limit(1) - .single() - expect(Array.isArray(res.data?.user_profiles)).toBe(true) - expect(res).toMatchInlineSnapshot(` - Object { - "count": null, - "data": Object { - "user_profiles": Array [], - }, - "error": null, - "status": 200, - "statusText": "OK", - } - `) -}) - -test('join on 1-M relation', async () => { - // TODO: This won't raise the proper types for "first_friend_of,..." results - const res = await postgrest - .from('users') - .select( - `first_friend_of:best_friends_first_user_fkey(*), - second_friend_of:best_friends_second_user_fkey(*), - third_wheel_of:best_friends_third_wheel_fkey(*)` - ) - .eq('username', 'supabot') - .limit(1) - .single() - expect(Array.isArray(res.data?.first_friend_of)).toBe(true) - expect(Array.isArray(res.data?.second_friend_of)).toBe(true) - expect(Array.isArray(res.data?.third_wheel_of)).toBe(true) - expect(res).toMatchInlineSnapshot(` - Object { - "count": null, - "data": Object { - "first_friend_of": Array [ - Object { - "first_user": "supabot", - "id": 1, - "second_user": "kiwicopple", - "third_wheel": "awailas", - }, - Object { - "first_user": "supabot", - "id": 2, - "second_user": "awailas", - "third_wheel": null, - }, - ], - "second_friend_of": Array [], - "third_wheel_of": Array [], - }, - "error": null, - "status": 200, - "statusText": "OK", - } - `) -}) - -test('join on 1-1 relation with nullables', async () => { - const res = await postgrest - .from('best_friends') - .select( - 'first_user:users!best_friends_first_user_fkey(*), second_user:users!best_friends_second_user_fkey(*), third_wheel:users!best_friends_third_wheel_fkey(*)' - ) - .order('id') - .limit(1) - .single() - expect(Array.isArray(res.data?.first_user)).toBe(false) - expect(Array.isArray(res.data?.second_user)).toBe(false) - expect(Array.isArray(res.data?.third_wheel)).toBe(false) - // TODO: This should return null only if the column is actually nullable thoses are not - expect(res.data?.first_user?.username).not.toBeNull() - expect(res.data?.second_user?.username).not.toBeNull() - // TODO: This column however is nullable - expect(res.data?.third_wheel?.username).not.toBeNull() - expect(res).toMatchInlineSnapshot(` - Object { - "count": null, - "data": Object { - "first_user": Object { - "age_range": "[1,2)", - "catchphrase": "'cat' 'fat'", - "data": null, - "status": "ONLINE", - "username": "supabot", - }, - "second_user": Object { - "age_range": "[25,35)", - "catchphrase": "'bat' 'cat'", - "data": null, - "status": "OFFLINE", - "username": "kiwicopple", - }, - "third_wheel": Object { - "age_range": "[25,35)", - "catchphrase": "'bat' 'rat'", - "data": null, - "status": "ONLINE", - "username": "awailas", - }, - }, - "error": null, - "status": 200, - "statusText": "OK", - } - `) -}) diff --git a/test/db/docker-compose.yml b/test/db/docker-compose.yml index 13e9d2d7..ac2f5e1c 100644 --- a/test/db/docker-compose.yml +++ b/test/db/docker-compose.yml @@ -3,7 +3,7 @@ version: '3' services: rest: - image: postgrest/postgrest:v11.2.2 + image: postgrest/postgrest:v12.2.0 ports: - '3000:3000' environment: @@ -13,6 +13,7 @@ services: PGRST_DB_ANON_ROLE: postgres PGRST_DB_PLAN_ENABLED: 1 PGRST_DB_TX_END: commit-allow-override + PGRST_DB_AGGREGATES_ENABLED: true depends_on: - db db: diff --git a/test/index.test-d.ts b/test/index.test-d.ts index c1fb9ba0..2abe2942 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -1,6 +1,5 @@ import { expectError, expectType } from 'tsd' -import { PostgrestClient, PostgrestSingleResponse } from '../src/index' -import { SelectQueryError } from '../src/select-query-parser' +import { PostgrestClient } from '../src/index' import { Prettify } from '../src/types' import { Database, Json } from './types' @@ -141,62 +140,6 @@ const postgrest = new PostgrestClient(REST_URL) expectType<'ONLINE' | 'OFFLINE'>(data) } -// many-to-one relationship -{ - const { data: message, error } = await postgrest.from('messages').select('user:users(*)').single() - if (error) { - throw new Error(error.message) - } - expectType(message.user) -} - -// !inner relationship -{ - const { data: message, error } = await postgrest - .from('messages') - .select('channels!inner(*, channel_details!inner(*))') - .single() - if (error) { - throw new Error(error.message) - } - type ExpectedType = Prettify< - Database['public']['Tables']['channels']['Row'] & { - channel_details: Database['public']['Tables']['channel_details']['Row'] - } - > - - expectType(message.channels) -} - -// one-to-many relationship -{ - const { data: user, error } = await postgrest.from('users').select('messages(*)').single() - if (error) { - throw new Error(error.message) - } - expectType(user.messages) -} - -// referencing missing column -{ - const res = await postgrest.from('users').select('username, dat') - expectType[]>>(res) -} - -// one-to-one relationship -{ - const { data: channels, error } = await postgrest - .from('channels') - .select('channel_details(*)') - .single() - if (error) { - throw new Error(error.message) - } - expectType( - channels.channel_details - ) -} - // PostgrestBuilder's children retains class when using inherited methods { const x = postgrest.from('channels').select() @@ -205,83 +148,3 @@ const postgrest = new PostgrestClient(REST_URL) expectType(y) expectType(z) } - -// !left oneToOne -{ - const { data: oneToOne, error } = await postgrest - .from('channel_details') - .select('channels!left(*)') - .single() - - if (error) { - throw new Error(error.message) - } - - // TODO: this should never be nullable - expectType(oneToOne.channels) -} - -// !left oneToMany -{ - const { data: oneToMany, error } = await postgrest - .from('users') - .select('messages!left(*)') - .single() - - if (error) { - throw new Error(error.message) - } - - expectType>(oneToMany.messages) -} - -// !left zeroToOne -{ - const { data: zeroToOne, error } = await postgrest - .from('user_profiles') - .select('users!left(*)') - .single() - - if (error) { - throw new Error(error.message) - } - - expectType(zeroToOne.users) -} - -// join over a 1-1 relation with both nullables and non-nullables fields -{ - const { data: bestFriends, error } = await postgrest - .from('best_friends') - .select( - 'first_user:users!best_friends_first_user_fkey(*), second_user:users!best_friends_second_user_fkey(*), third_wheel:users!best_friends_third_wheel_fkey(*)' - ) - .single() - - if (error) { - throw new Error(error.message) - } - - // TODO: Those two fields shouldn't be nullables - expectType(bestFriends.first_user) - expectType(bestFriends.second_user) - // The third wheel should be nullable - expectType(bestFriends.third_wheel) -} -// join over a 1-M relation with both nullables and non-nullables fields -{ - const { data: users, error } = await postgrest - .from('users') - .select( - `first_friend_of:best_friends_first_user_fkey(*), - second_friend_of:best_friends_second_user_fkey(*), - third_wheel_of:best_friends_third_wheel_fkey(*)` - ) - .single() - - if (error) { - throw new Error(error.message) - } - // TODO: type properly the result for this kind of queries - expectType>(users.first_friend_of) -} diff --git a/test/index.test.ts b/test/index.test.ts index ad4e4ee7..6a18f85d 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,4 +1,5 @@ import './basic' +import './relationships' import './filters' import './resource-embedding' import './transforms' diff --git a/test/relationships.test-d.ts b/test/relationships.test-d.ts new file mode 100644 index 00000000..cb2646e3 --- /dev/null +++ b/test/relationships.test-d.ts @@ -0,0 +1,569 @@ +import { expectType } from 'tsd' +import { TypeEqual } from 'ts-expect' +import { SelectQueryError } from '../src/select-query-parser' +import { Prettify } from '../src/types' +import { Database } from './types' +import { selectQueries } from './relationships' + +// nested query with selective fields +{ + const { data, error } = await selectQueries.nestedQueryWithSelectiveFields.limit(1).single() + if (error) { + throw new Error(error.message) + } + expectType>(data) + // we need ts-expect TypeEqual here to ensure never properties wont pass the test + expectType[], typeof data.messages>>(true) +} + +// nested query with multiple levels and selective fields +{ + const { data, error } = await selectQueries.nestedQueryWithMultipleLevelsAndSelectiveFields.limit(1).single() + if (error) { + throw new Error(error.message) + } + // Complex nested types combination and picking make ts-expect fail + type ExpectedType = { + messages: Array<{ + id: number; + message: string | null; + channels: { + id: number; + slug: string | null; + }; + }>; + username: string; + } + expectType>(true) +} + +// query with multiple one-to-many relationships +{ + const { data, error } = await selectQueries.queryWithMultipleOneToManySelectives.limit(1).single() + if (error) { + throw new Error(error.message) + } + expectType>(data) + expectType>, typeof data.messages>>(true) + expectType>, typeof data.user_profiles>>(true) +} + +// many-to-one relationship +{ + const { data: message, error } = await selectQueries.manyToOne.single() + if (error) { + throw new Error(error.message) + } + expectType(message.user) +} + +// !inner relationship +{ + const { data: message, error } = await selectQueries.inner + .single() + if (error) { + throw new Error(error.message) + } + type ExpectedType = Prettify< + Database['public']['Tables']['channels']['Row'] & { + channel_details: Database['public']['Tables']['channel_details']['Row'] + } + > + + expectType>(true) +} + +// one-to-many relationship +{ + const { data: user, error } = await selectQueries.oneToMany.single() + if (error) { + throw new Error(error.message) + } + expectType(user.messages) +} + +// one-to-many relationship with selective columns +{ + const { data: user, error } = await selectQueries.oneToManySelective.single() + if (error) { + throw new Error(error.message) + } + type ExpectedType = { + messages: Array> + } + expectType>(true) +} + + +// one-to-one relationship +{ + const { data: channels, error } = await selectQueries.oneToOne + .single() + if (error) { + throw new Error(error.message) + } + expectType( + channels.channel_details + ) +} + +// !left oneToOne +{ + const { data: oneToOne, error } = await selectQueries.leftOneToOne + .single() + + if (error) { + throw new Error(error.message) + } + + expectType(oneToOne.channels) +} + +// !left oneToMany +{ + const { data: oneToMany, error } = await selectQueries.leftOneToMany + .single() + + if (error) { + throw new Error(error.message) + } + + expectType, typeof oneToMany.messages>>(true) +} + +// !left zeroToOne +{ + const { data: zeroToOne, error } = await selectQueries.leftZeroToOne + .single() + + if (error) { + throw new Error(error.message) + } + + expectType(zeroToOne.users) +} + +// join over a 1-M relation with both nullables and non-nullables fields using foreign key name for hinting +{ + const { data: users, error } = await selectQueries.joinOneToManyWithFkHint + .single() + + if (error) { + throw new Error(error.message) + } + expectType, typeof users.first_friend_of>>(true) +} + +// join over a 1-1 relation with both nullables and non-nullables fields with no hinting +{ + const { data, error } = await selectQueries.joinOneToOneWithNullablesNoHint + .single() + + if (error) { + throw new Error(error.message) + } + expectType ?">>(data.first_user) +} + +// join over a 1-1 relation with both nullablesand non-nullables fields with column name hinting +{ + const { data, error } = await selectQueries.joinOneToOneWithNullablesColumnHint + .single() + + if (error) { + throw new Error(error.message) + } + expectType(data.first_user) + expectType(data.second_user) + expectType(data.third_wheel) +} + +// join over a 1-M relation with both nullables and non-nullables fields with no hinting +{ + const { data, error } = await selectQueries.joinOneToManyWithNullablesNoHint + .single() + + if (error) { + throw new Error(error.message) + } + expectType ?">>(data.first_friend_of) +} + +// join over a 1-M relation with both nullables and non-nullables fields using column name for hinting +{ + const { data, error } = await selectQueries.joinOneToManyWithNullablesColumnHint + .single() + + if (error) { + throw new Error(error.message) + } + expectType>(data.first_friend_of) + expectType>(data.second_friend_of) + expectType>(data.third_wheel_of) +} + +// join over a 1-M relation with both nullables and non-nullables fields using column name hinting on nested relation +{ + const { data, error } = await selectQueries.joinOneToManyWithNullablesColumnHintOnNestedRelation + .single() + + if (error) { + throw new Error(error.message) + } + type ExpectedType = Prettify + expectType>(true) + expectType, typeof data.second_friend_of>>(true) + expectType, typeof data.third_wheel_of>>(true) +} + +// join over a 1-M relation with both nullables and non-nullables fields using no hinting on nested relation +{ + const { data, error } = await selectQueries.joinOneToManyWithNullablesNoHintOnNestedRelation + .single() + + if (error) { + throw new Error(error.message) + } + expectType ?">>(data.first_friend_of[0].first_user) +} + +// !left join on one to 0-1 non-empty relation +{ + const { data, error } = await selectQueries.leftOneToOneUsers.single() + + if (error) { + throw new Error(error.message) + } + expectType>, typeof data.user_profiles>>(true) +} + +// join on one to 0-1 non-empty relation via column name +{ + const { data, error } = await selectQueries.oneToOneUsersColumnName.single() + + if (error) { + throw new Error(error.message) + } + expectType>, typeof data.user_profiles>>(true) +} + +// !left join on zero to one with null relation +{ + const { data, error } = await selectQueries.leftZeroToOneUserProfiles.single() + + if (error) { + throw new Error(error.message) + } + expectType>(true) +} + +// !left join on zero to one with valid relation +{ + const { data, error } = await selectQueries.leftZeroToOneUserProfilesWithNullables.single() + + if (error) { + throw new Error(error.message) + } + expectType | null, typeof data.users>>(true) +} + +// !left join on zero to one empty relation +{ + const { data, error } = await selectQueries.leftOneToOneUsers.single() + + if (error) { + throw new Error(error.message) + } + expectType>, typeof data.user_profiles>>(true) +} + +// join select via unique table relationship +{ + const { data, error } = await selectQueries.joinSelectViaUniqueTableRelationship.limit(1).single() + + if (error) { + throw new Error(error.message) + } + expectType>(true) +} + +// join select via view name relationship +{ + const { data, error } = await selectQueries.joinSelectViaViewNameRelationship.limit(1).single() + + if (error) { + throw new Error(error.message) + } + expectType>(true) +} + +// select with aggregate count function +{ + const { data, error } = await selectQueries.selectWithAggregateCountFunction.limit(1).single() + + if (error) { + throw new Error(error.message) + } + type ExpectedType = { + username: string + messages: Array<{ + count: number + }> + } + expectType>(true) +} + +// join on 1-1 relation with nullables +{ + const { data, error } = await selectQueries.joinOneToOneWithNullablesFkHint + .single() + + if (error) { + throw new Error(error.message) + } + expectType(data.first_user) + expectType(data.second_user) + // This one might be null + expectType(data.third_wheel) +} + +// join over a 1-1 relation with both nullables and non-nullables fields using foreign key name for hinting +{ + const { data: bestFriends, error } = await selectQueries.joinOneToOneWithFkHint + .single() + + if (error) { + throw new Error(error.message) + } + + expectType(bestFriends.first_user) + expectType(bestFriends.second_user) + // The third wheel should be nullable + expectType(bestFriends.third_wheel) +} + +// select with nested aggregate count function +{ + const { data, error } = await selectQueries.selectWithAggregateNestedCountFunction.limit(1).single() + + if (error) { + throw new Error(error.message) + } + type ExpectedType = { + username: string + messages: Array<{ + channels: { + count: number + } + }> + } + expectType>(true) +} + +// select with aggregate sum function on nested relation +{ + const { data, error } = await selectQueries.selectWithAggregateSumFunctionOnNestedRelation.limit(1).single() + + if (error) { + throw new Error(error.message) + } + + type ExpectedType = { + username: string + messages: Array<{ + channels: { + sum: number + } + }> + } + expectType>(true) +} + +// select with aggregate sum and spread +{ + const { data, error } = await selectQueries.selectWithAggregateSumAndSpread.limit(1).single() + + if (error) { + throw new Error(error.message) + } + type ExpectedType = { + username: string + messages: Array<{ + channels: { + sum: number + details: string | null + } + }> + } + expectType>(true) +} + +// select with aggregate sum function +{ + const { data, error } = await selectQueries.selectWithAggregateSumFunction.limit(1).single() + + if (error) { + throw new Error(error.message) + } + type ExpectedType = { + username: string + messages: Array<{ + sum: number + }> + } + expectType>(true) +} + +// join on 1-M relation with selective fk hinting +{ + const { data, error } = await selectQueries.joinOneToManyUsersWithFkHintSelective.limit(1).single() + + if (error) { + throw new Error(error.message) + } + expectType>, typeof data.first_friend_of>>(true) + expectType, typeof data.second_friend_of>>(true) + expectType, typeof data.third_wheel_of>>(true) +} + +// join on 1-M relation +{ + const { data, error } = await selectQueries.joinOneToManyUsersWithFkHint + .single() + + if (error) { + throw new Error(error.message) + } + expectType>(data.first_friend_of) + expectType>(data.second_friend_of) + expectType>(data.third_wheel_of) +} + +// add a test with type casting +{ + const { data, error } = await selectQueries.typeCastingQuery.single() + + if (error) { + throw new Error(error.message) + } + + type ExpectedType = { + id: string + } + + expectType>(true) +} + +// TODO: From here live the dragons and errors + + +// join select via column +{ + const { data, error } = await selectQueries.joinSelectViaColumn.limit(1).single() + + if (error) { + throw new Error(error.message) + } + expectType>(true) +} + +// join select via column and alias +{ + const { data, error } = await selectQueries.joinSelectViaColumnAndAlias.limit(1).single() + + if (error) { + throw new Error(error.message) + } + expectType>(true) +} + +// select with aggregate count function and alias +{ + const { data, error } = await selectQueries.selectWithAggregateCountFunctionAndAlias.limit(1).single() + + if (error) { + throw new Error(error.message) + } + type ExpectedType = { + username: string + messages: Array<{ + message_count: number + }> + } + expectType>(true) +} + +// select with aggregate nested count function and alias +{ + const { data, error } = await selectQueries.selectWithAggregateNestedCountFunctionAndAlias.limit(1).single() + + if (error) { + throw new Error(error.message) + } + type ExpectedType = { + username: string + messages: Array<{ + channels: { + channel_count: number + } | null + }> + } + expectType>(true) +} + +// select with aggregate count and spread +{ + const { data, error } = await selectQueries.selectWithAggregateCountAndSpread.limit(1).single() + + if (error) { + throw new Error(error.message) + } + type ExpectedType = { + username: string + messages: Array<{ + channels: { + count: number + details: string | null + } + }> + } + expectType>(true) +} + +// select with aggregate aliased sum function +{ + const { data, error } = await selectQueries.selectWithAggregateSumFunction.limit(1).single() + + if (error) { + throw new Error(error.message) + } + type ExpectedType = { + username: string + messages: Array<{ + sum_id: number + }> + } + expectType>(true) +} + +// select with aggregate sum and spread on nested relation +{ + const { data, error } = await selectQueries.selectWithAggregateSumAndSpreadOnNestedRelation.limit(1).single() + + if (error) { + throw new Error(error.message) + } + type ExpectedType = { + username: string + messages: Array<{ + channels: { + sum: number + details_sum: number + details: string | null + } + }> + } + expectType>(true) +} diff --git a/test/relationships.ts b/test/relationships.ts new file mode 100644 index 00000000..2d496081 --- /dev/null +++ b/test/relationships.ts @@ -0,0 +1,1065 @@ +import { PostgrestClient } from '../src/index' +import { Database } from './types' + +const REST_URL = 'http://localhost:3000' +const postgrest = new PostgrestClient(REST_URL) + +const userColumn: 'catchphrase' | 'username' = 'username' + +export const selectQueries = { + manyToOne: postgrest.from('messages').select('user:users(*)'), + inner: postgrest.from('messages').select('channels!inner(*, channel_details!inner(*))'), + oneToMany: postgrest.from('users').select('messages(*)'), + oneToManySelective: postgrest.from('users').select('messages(data)'), + oneToOne: postgrest.from('channels').select('channel_details(*)'), + leftOneToOne: postgrest.from('channel_details').select('channels!left(*)'), + leftOneToMany: postgrest.from('users').select('messages!left(*)'), + leftZeroToOne: postgrest.from('user_profiles').select('users!left(*)'), + leftOneToOneUsers: postgrest.from('users').select('user_profiles!left(username)'), + oneToOneUsersColumnName: postgrest.from('users').select('user_profiles(username)'), + leftZeroToOneUserProfiles: postgrest.from('user_profiles').select('*,users!left(*)'), + leftZeroToOneUserProfilesWithNullables: postgrest + .from('user_profiles') + .select('*,users!left(status)'), + joinOneToOne: postgrest.from('channel_details').select('channels!left(id)'), + joinOneToMany: postgrest.from('users').select('messages!left(username)'), + joinZeroToOne: postgrest.from('user_profiles').select('users!left(status)'), + joinOneToOneWithFkHint: postgrest + .from('best_friends') + .select( + 'first_user:users!best_friends_first_user_fkey(*), second_user:users!best_friends_second_user_fkey(*), third_wheel:users!best_friends_third_wheel_fkey(*)' + ), + joinOneToManyWithFkHint: postgrest.from('users') + .select(`first_friend_of:best_friends!best_friends_first_user_fkey(*), + second_friend_of:best_friends!best_friends_second_user_fkey(*), + third_wheel_of:best_friends!best_friends_third_wheel_fkey(*)`), + joinOneToManyUsersWithFkHint: postgrest.from('users').select( + `first_friend_of:best_friends_first_user_fkey(*), + second_friend_of:best_friends_second_user_fkey(*), + third_wheel_of:best_friends_third_wheel_fkey(*)` + ), + joinOneToManyUsersWithFkHintSelective: postgrest.from('users').select( + `first_friend_of:best_friends_first_user_fkey(id), + second_friend_of:best_friends_second_user_fkey(*), + third_wheel_of:best_friends_third_wheel_fkey(*)` + ), + joinOneToOneWithNullablesFkHint: postgrest + .from('best_friends') + .select( + 'first_user:users!best_friends_first_user_fkey(*), second_user:users!best_friends_second_user_fkey(*), third_wheel:users!best_friends_third_wheel_fkey(*)' + ), + joinOneToOneWithNullablesNoHint: postgrest + .from('best_friends') + .select('first_user:users(*), second_user:users(*), third_wheel:users(*)'), + joinOneToOneWithNullablesColumnHint: postgrest + .from('best_friends') + .select( + 'first_user:users!first_user(*), second_user:users!second_user(*), third_wheel:users!third_wheel(*)' + ), + joinOneToManyWithNullablesNoHint: postgrest + .from('users') + .select( + 'first_friend_of:best_friends(*), second_friend_of:best_friends(*), third_wheel_of:best_friends(*)' + ), + joinOneToManyWithNullablesColumnHint: postgrest + .from('users') + .select( + 'first_friend_of:best_friends!first_user(*), second_friend_of:best_friends!second_user(*), third_wheel_of:best_friends!third_wheel(*)' + ), + joinOneToManyWithNullablesColumnHintOnNestedRelation: postgrest + .from('users') + .select( + 'first_friend_of:best_friends!first_user(*, first_user:users!first_user(*)), second_friend_of:best_friends!second_user(*), third_wheel_of:best_friends!third_wheel(*)' + ), + joinOneToManyWithNullablesNoHintOnNestedRelation: postgrest + .from('users') + .select( + 'first_friend_of:best_friends!first_user(*, first_user:users(*)), second_friend_of:best_friends!second_user(*), third_wheel_of:best_friends!third_wheel(*)' + ), + joinSelectViaColumn: postgrest.from('user_profiles').select('username(*)'), + joinSelectViaColumnSelective: postgrest.from('user_profiles').select('username(status)'), + joinSelectViaColumnAndAlias: postgrest.from('user_profiles').select('user:username(*)'), + joinSelectViaUniqueTableRelationship: postgrest.from('user_profiles').select('users(*)'), + typeCastingQuery: postgrest.from('best_friends').select('id::text'), + joinSelectViaViewNameRelationship: postgrest.from('user_profiles').select('updatable_view(*)'), + queryWithMultipleOneToManySelectives: postgrest + .from('users') + .select('username, messages(id), user_profiles(id)'), + nestedQueryWithMultipleLevelsAndSelectiveFields: postgrest + .from('users') + .select('username, messages(id, message, channels(id, slug))'), + nestedQueryWithSelectiveFields: postgrest.from('users').select('username, messages(id, message)'), + selectionWithStringTemplating: postgrest.from('users').select(`status, ${userColumn}`), + selectWithAggregateCountFunction: postgrest.from('users').select('username, messages(count)'), + selectWithAggregateCountFunctionAndAlias: postgrest + .from('users') + .select('username, messages(message_count:count())'), + selectWithAggregateNestedCountFunction: postgrest + .from('users') + .select('username, messages(channels(count))'), + selectWithAggregateNestedCountFunctionAndAlias: postgrest + .from('users') + .select('username, messages(channels(channel_count:count()))'), + selectWithAggregateCountAndSpread: postgrest + .from('users') + .select('username, messages(channels(count(), ...channel_details(details)))'), + selectWithAggregateSumFunction: postgrest.from('users').select('username, messages(id.sum())'), + selectWithAggregateAliasedSumFunction: postgrest + .from('users') + .select('username, messages(sum_id:id.sum())'), + selectWithAggregateSumFunctionOnNestedRelation: postgrest + .from('users') + .select('username, messages(channels(id.sum()))'), + selectWithAggregateSumAndSpread: postgrest + .from('users') + .select('username, messages(channels(id.sum(), ...channel_details(details)))'), + selectWithAggregateSumAndSpreadOnNestedRelation: postgrest + .from('users') + .select( + 'username, messages(channels(id.sum(), ...channel_details(details_sum:id.sum(), details)))' + ), +} + +test('nested query with selective fields', async () => { + const { data, error } = await selectQueries.nestedQueryWithSelectiveFields.limit(1).single() + expect(error).toBeNull() + expect(data).toMatchInlineSnapshot(` + Object { + "messages": Array [ + Object { + "id": 1, + "message": "Hello World 👋", + }, + Object { + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + }, + ], + "username": "supabot", + } + `) +}) + +test('nested query with multiple levels and selective fields', async () => { + const { data, error } = await selectQueries.nestedQueryWithMultipleLevelsAndSelectiveFields + .limit(1) + .single() + expect(error).toBeNull() + expect(data).toMatchInlineSnapshot(` + Object { + "messages": Array [ + Object { + "channels": Object { + "id": 1, + "slug": "public", + }, + "id": 1, + "message": "Hello World 👋", + }, + Object { + "channels": Object { + "id": 2, + "slug": "random", + }, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + }, + ], + "username": "supabot", + } + `) +}) + +test('query with multiple one-to-many relationships', async () => { + const { data, error } = await selectQueries.queryWithMultipleOneToManySelectives.limit(1).single() + expect(error).toBeNull() + expect(data).toMatchInlineSnapshot(` + Object { + "messages": Array [ + Object { + "id": 1, + }, + Object { + "id": 2, + }, + ], + "user_profiles": Array [ + Object { + "id": 1, + }, + ], + "username": "supabot", + } + `) +}) + +test('many-to-one relationship', async () => { + const { data: message, error } = await selectQueries.manyToOne.limit(1).single() + expect(error).toBeNull() + expect(message).toBeDefined() + expect(message!.user).toBeDefined() + expect(typeof message!.user).toBe('object') + expect(message).toMatchInlineSnapshot(` + Object { + "user": Object { + "age_range": "[1,2)", + "catchphrase": "'cat' 'fat'", + "data": null, + "status": "ONLINE", + "username": "supabot", + }, + } + `) +}) + +test('!inner relationship', async () => { + const { data: message, error } = await selectQueries.inner.limit(1).single() + expect(error).toBeNull() + expect(message).toBeDefined() + expect(message!.channels).toBeDefined() + expect(typeof message!.channels).toBe('object') + expect(message!.channels.channel_details).toBeDefined() + expect(typeof message!.channels.channel_details).toBe('object') + expect(message).toMatchInlineSnapshot(` + Object { + "channels": Object { + "channel_details": Object { + "details": "Details for public channel", + "id": 1, + }, + "data": null, + "id": 1, + "slug": "public", + }, + } + `) +}) + +test('one-to-many relationship', async () => { + const { data: user, error } = await selectQueries.oneToMany.limit(1).single() + expect(error).toBeNull() + expect(user).toBeDefined() + expect(Array.isArray(user!.messages)).toBe(true) + expect(user).toMatchInlineSnapshot(` + Object { + "messages": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + ], + } + `) +}) + +test('one-to-many relationship with selective columns', async () => { + const { data: user, error } = await selectQueries.oneToManySelective.limit(1).single() + expect(error).toBeNull() + expect(user).toBeDefined() + expect(Array.isArray(user!.messages)).toBe(true) + expect(user).toMatchInlineSnapshot(` + Object { + "messages": Array [ + Object { + "data": null, + }, + Object { + "data": null, + }, + ], + } + `) +}) + +test('one-to-one relationship', async () => { + const { data: channels, error } = await selectQueries.oneToOne.limit(1).single() + expect(error).toBeNull() + expect(channels).toBeDefined() + expect(channels!.channel_details).toBeDefined() + expect(typeof channels!.channel_details).toBe('object') +}) + +test('!left oneToOne', async () => { + const { data: oneToOne, error } = await selectQueries.leftOneToOne.limit(1).single() + + expect(error).toBeNull() + expect(oneToOne).toBeDefined() + expect(oneToOne!.channels).toBeDefined() + expect(typeof oneToOne!.channels).toBe('object') +}) + +test('!left oneToMany', async () => { + const { data: oneToMany, error } = await selectQueries.leftOneToMany.limit(1).single() + + expect(error).toBeNull() + expect(oneToMany).toBeDefined() + expect(Array.isArray(oneToMany!.messages)).toBe(true) + expect(oneToMany).toMatchInlineSnapshot(` + Object { + "messages": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + ], + } + `) +}) + +test('!left zeroToOne', async () => { + const { data: zeroToOne, error } = await selectQueries.leftZeroToOne.limit(1).single() + + expect(error).toBeNull() + expect(zeroToOne).toBeDefined() + expect(zeroToOne!.users).toBeDefined() + expect(typeof zeroToOne!.users).toBe('object') +}) + +test('join over a 1-1 relation with both nullables and non-nullables fields using foreign key name for hinting', async () => { + const { data: bestFriends, error } = await selectQueries.joinOneToOneWithFkHint.limit(1).single() + + expect(error).toBeNull() + expect(bestFriends).toBeDefined() + expect(bestFriends!.first_user).toBeDefined() + expect(bestFriends!.second_user).toBeDefined() + expect(bestFriends!.third_wheel).toBeDefined() + expect(typeof bestFriends!.first_user).toBe('object') + expect(typeof bestFriends!.second_user).toBe('object') + expect(typeof bestFriends!.third_wheel).toBe('object') +}) + +test('join over a 1-M relation with both nullables and non-nullables fields using foreign key name for hinting', async () => { + const { data: users, error } = await selectQueries.joinOneToManyWithFkHint.limit(1).single() + + expect(error).toBeNull() + expect(users).toBeDefined() + expect(Array.isArray(users!.first_friend_of)).toBe(true) + expect(Array.isArray(users!.second_friend_of)).toBe(true) + expect(Array.isArray(users!.third_wheel_of)).toBe(true) + expect(typeof users!.first_friend_of[0]).toBe('object') +}) + +test('join on 1-M relation', async () => { + const res = await selectQueries.joinOneToManyUsersWithFkHint + .eq('username', 'supabot') + .limit(1) + .single() + expect(Array.isArray(res.data?.first_friend_of)).toBe(true) + expect(Array.isArray(res.data?.second_friend_of)).toBe(true) + expect(Array.isArray(res.data?.third_wheel_of)).toBe(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "first_friend_of": Array [ + Object { + "first_user": "supabot", + "id": 1, + "second_user": "kiwicopple", + "third_wheel": "awailas", + }, + Object { + "first_user": "supabot", + "id": 2, + "second_user": "awailas", + "third_wheel": null, + }, + ], + "second_friend_of": Array [], + "third_wheel_of": Array [], + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('join on 1-1 relation with nullables', async () => { + const res = await selectQueries.joinOneToOneWithNullablesFkHint.order('id').limit(1).single() + expect(Array.isArray(res.data?.first_user)).toBe(false) + expect(Array.isArray(res.data?.second_user)).toBe(false) + expect(Array.isArray(res.data?.third_wheel)).toBe(false) + // TODO: This should return null only if the column is actually nullable thoses are not + expect(res.data?.first_user?.username).not.toBeNull() + expect(res.data?.second_user?.username).not.toBeNull() + // TODO: This column however is nullable + expect(res.data?.third_wheel?.username).not.toBeNull() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "first_user": Object { + "age_range": "[1,2)", + "catchphrase": "'cat' 'fat'", + "data": null, + "status": "ONLINE", + "username": "supabot", + }, + "second_user": Object { + "age_range": "[25,35)", + "catchphrase": "'bat' 'cat'", + "data": null, + "status": "OFFLINE", + "username": "kiwicopple", + }, + "third_wheel": Object { + "age_range": "[25,35)", + "catchphrase": "'bat' 'rat'", + "data": null, + "status": "ONLINE", + "username": "awailas", + }, + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('join over a 1-1 relation with both nullables and non-nullables fields with no hinting', async () => { + const { error } = await selectQueries.joinOneToOneWithNullablesNoHint.single() + + expect(error).toMatchInlineSnapshot(` + Object { + "code": "PGRST201", + "details": Array [ + Object { + "cardinality": "many-to-one", + "embedding": "best_friends with users", + "relationship": "best_friends_first_user_fkey using best_friends(first_user) and users(username)", + }, + Object { + "cardinality": "many-to-one", + "embedding": "best_friends with users", + "relationship": "best_friends_second_user_fkey using best_friends(second_user) and users(username)", + }, + Object { + "cardinality": "many-to-one", + "embedding": "best_friends with users", + "relationship": "best_friends_third_wheel_fkey using best_friends(third_wheel) and users(username)", + }, + ], + "hint": "Try changing 'users' to one of the following: 'users!best_friends_first_user_fkey', 'users!best_friends_second_user_fkey', 'users!best_friends_third_wheel_fkey'. Find the desired relationship in the 'details' key.", + "message": "Could not embed because more than one relationship was found for 'best_friends' and 'users'", + } + `) +}) + +test('join over a 1-1 relation with both nullablesand non-nullables fields with column name hinting', async () => { + const { data: bestFriends, error } = await selectQueries.joinOneToOneWithNullablesColumnHint + .limit(1) + .single() + expect(error).toBeNull() + expect(bestFriends).toBeDefined() + expect(bestFriends!.first_user).toBeDefined() + expect(bestFriends!.second_user).toBeDefined() + expect(bestFriends!.third_wheel).toBeDefined() + expect(typeof bestFriends!.first_user).toBe('object') + expect(typeof bestFriends!.second_user).toBe('object') + expect(typeof bestFriends!.third_wheel).toBe('object') +}) + +test('join over a 1-M relation with both nullables and non-nullables fields with no hinting', async () => { + const { error } = await selectQueries.joinOneToManyWithNullablesNoHint.limit(1).single() + expect(error).toMatchInlineSnapshot(` + Object { + "code": "PGRST201", + "details": Array [ + Object { + "cardinality": "one-to-many", + "embedding": "users with best_friends", + "relationship": "best_friends_first_user_fkey using users(username) and best_friends(first_user)", + }, + Object { + "cardinality": "one-to-many", + "embedding": "users with best_friends", + "relationship": "best_friends_second_user_fkey using users(username) and best_friends(second_user)", + }, + Object { + "cardinality": "one-to-many", + "embedding": "users with best_friends", + "relationship": "best_friends_third_wheel_fkey using users(username) and best_friends(third_wheel)", + }, + ], + "hint": "Try changing 'best_friends' to one of the following: 'best_friends!best_friends_first_user_fkey', 'best_friends!best_friends_second_user_fkey', 'best_friends!best_friends_third_wheel_fkey'. Find the desired relationship in the 'details' key.", + "message": "Could not embed because more than one relationship was found for 'users' and 'best_friends'", + } + `) +}) + +test('join over a 1-M relation with both nullables and non-nullables fields using column name for hinting', async () => { + const { data: users, error } = await selectQueries.joinOneToManyWithNullablesColumnHint + .limit(1) + .single() + expect(error).toBeNull() + expect(users).toBeDefined() + expect(Array.isArray(users!.first_friend_of)).toBe(true) + expect(Array.isArray(users!.second_friend_of)).toBe(true) + expect(Array.isArray(users!.third_wheel_of)).toBe(true) +}) + +test('join over a 1-M relation with both nullables and non-nullables fields using column name hinting on nested relation', async () => { + const { data: users, error } = + await selectQueries.joinOneToManyWithNullablesColumnHintOnNestedRelation.limit(1).single() + expect(error).toBeNull() + expect(users).toBeDefined() + expect(Array.isArray(users!.first_friend_of)).toBe(true) + expect(Array.isArray(users!.second_friend_of)).toBe(true) + expect(Array.isArray(users!.third_wheel_of)).toBe(true) + expect(typeof users?.first_friend_of[0]?.first_user).toBe('object') +}) + +test('join over a 1-M relation with both nullables and non-nullables fields using no hinting on nested relation', async () => { + const { error } = await selectQueries.joinOneToManyWithNullablesNoHintOnNestedRelation + .limit(1) + .single() + expect(error).toMatchInlineSnapshot(` + Object { + "code": "PGRST201", + "details": Array [ + Object { + "cardinality": "many-to-one", + "embedding": "best_friends with users", + "relationship": "best_friends_first_user_fkey using best_friends(first_user) and users(username)", + }, + Object { + "cardinality": "many-to-one", + "embedding": "best_friends with users", + "relationship": "best_friends_second_user_fkey using best_friends(second_user) and users(username)", + }, + Object { + "cardinality": "many-to-one", + "embedding": "best_friends with users", + "relationship": "best_friends_third_wheel_fkey using best_friends(third_wheel) and users(username)", + }, + ], + "hint": "Try changing 'users' to one of the following: 'users!best_friends_first_user_fkey', 'users!best_friends_second_user_fkey', 'users!best_friends_third_wheel_fkey'. Find the desired relationship in the 'details' key.", + "message": "Could not embed because more than one relationship was found for 'best_friends' and 'users'", + } + `) +}) + +test('!left join on one to 0-1 non-empty relation', async () => { + const res = await selectQueries.leftOneToOneUsers.eq('username', 'supabot').limit(1).single() + expect(Array.isArray(res.data?.user_profiles)).toBe(true) + expect(res.data?.user_profiles[0].username).not.toBeNull() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "user_profiles": Array [ + Object { + "username": "supabot", + }, + ], + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('join on one to 0-1 non-empty relation via column name', async () => { + const res = await selectQueries.oneToOneUsersColumnName + .eq('username', 'supabot') + .limit(1) + .single() + expect(res.error).toBeNull() + expect(Array.isArray(res.data?.user_profiles)).toBe(true) + expect(res.data?.user_profiles[0].username).not.toBeNull() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "user_profiles": Array [ + Object { + "username": "supabot", + }, + ], + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('!left join on zero to one with null relation', async () => { + const res = await selectQueries.leftZeroToOneUserProfiles.eq('id', 2).limit(1).single() + expect(Array.isArray(res.data?.users)).toBe(false) + expect(res.data?.users).toBeNull() + + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "id": 2, + "username": null, + "users": null, + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('!left join on zero to one with valid relation', async () => { + const res = await selectQueries.leftZeroToOneUserProfilesWithNullables + .eq('id', 1) + .limit(1) + .single() + expect(Array.isArray(res.data?.users)).toBe(false) + // TODO: This should be nullable indeed + expect(res.data?.users?.status).not.toBeNull() + + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "id": 1, + "username": "supabot", + "users": Object { + "status": "ONLINE", + }, + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('!left join on zero to one empty relation', async () => { + const res = await selectQueries.leftOneToOneUsers.eq('username', 'dragarcia').limit(1).single() + expect(res.data).toBeNull() +}) + +test('join on 1-M relation with selective fk hinting', async () => { + const res = await selectQueries.joinOneToManyUsersWithFkHintSelective.limit(1).single() + expect(Array.isArray(res.data?.first_friend_of)).toBe(true) + expect(Array.isArray(res.data?.second_friend_of)).toBe(true) + expect(Array.isArray(res.data?.third_wheel_of)).toBe(true) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "first_friend_of": Array [ + Object { + "id": 1, + }, + Object { + "id": 2, + }, + ], + "second_friend_of": Array [], + "third_wheel_of": Array [], + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('join select via column', async () => { + const res = await selectQueries.joinSelectViaColumn.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "username": Object { + "age_range": "[1,2)", + "catchphrase": "'cat' 'fat'", + "data": null, + "status": "ONLINE", + "username": "supabot", + }, + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('join select via column selective', async () => { + const res = await selectQueries.joinSelectViaColumnSelective.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "username": Object { + "status": "ONLINE", + }, + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('join select via column and alias', async () => { + const res = await selectQueries.joinSelectViaColumnAndAlias.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "user": Object { + "age_range": "[1,2)", + "catchphrase": "'cat' 'fat'", + "data": null, + "status": "ONLINE", + "username": "supabot", + }, + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('join select via unique table relationship', async () => { + const res = await selectQueries.joinSelectViaUniqueTableRelationship.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "users": Object { + "age_range": "[1,2)", + "catchphrase": "'cat' 'fat'", + "data": null, + "status": "ONLINE", + "username": "supabot", + }, + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) +test('join select via view name relationship', async () => { + const res = await selectQueries.joinSelectViaViewNameRelationship.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "updatable_view": Object { + "non_updatable_column": 1, + "username": "supabot", + }, + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('join select via column with string templating', async () => { + const res = await selectQueries.selectionWithStringTemplating.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "status": "ONLINE", + "username": "supabot", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('select with aggregate count function', async () => { + const res = await selectQueries.selectWithAggregateCountFunction.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "messages": Array [ + Object { + "count": 2, + }, + ], + "username": "supabot", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('select with aggregate count function and alias', async () => { + const res = await selectQueries.selectWithAggregateCountFunctionAndAlias.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "messages": Array [ + Object { + "message_count": 2, + }, + ], + "username": "supabot", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('select with aggregate nested count function', async () => { + const res = await selectQueries.selectWithAggregateNestedCountFunction.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "messages": Array [ + Object { + "channels": Object { + "count": 1, + }, + }, + Object { + "channels": Object { + "count": 1, + }, + }, + ], + "username": "supabot", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('select with aggregate nested count function and alias', async () => { + const res = await selectQueries.selectWithAggregateNestedCountFunctionAndAlias.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "messages": Array [ + Object { + "channels": Object { + "channel_count": 1, + }, + }, + Object { + "channels": Object { + "channel_count": 1, + }, + }, + ], + "username": "supabot", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('select with aggregate count and spread', async () => { + const res = await selectQueries.selectWithAggregateCountAndSpread.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "messages": Array [ + Object { + "channels": Object { + "count": 1, + "details": "Details for public channel", + }, + }, + Object { + "channels": Object { + "count": 1, + "details": "Details for random channel", + }, + }, + ], + "username": "supabot", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('select with aggregate sum function', async () => { + const res = await selectQueries.selectWithAggregateSumFunction.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "messages": Array [ + Object { + "sum": 3, + }, + ], + "username": "supabot", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('select with aggregate aliased sum function', async () => { + const res = await selectQueries.selectWithAggregateAliasedSumFunction.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "messages": Array [ + Object { + "sum_id": 3, + }, + ], + "username": "supabot", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('select with aggregate sum function on nested relation', async () => { + const res = await selectQueries.selectWithAggregateSumFunctionOnNestedRelation.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "messages": Array [ + Object { + "channels": Object { + "sum": 1, + }, + }, + Object { + "channels": Object { + "sum": 2, + }, + }, + ], + "username": "supabot", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('select with aggregate sum and spread', async () => { + const res = await selectQueries.selectWithAggregateSumAndSpread.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "messages": Array [ + Object { + "channels": Object { + "details": "Details for public channel", + "sum": 1, + }, + }, + Object { + "channels": Object { + "details": "Details for random channel", + "sum": 2, + }, + }, + ], + "username": "supabot", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('select with aggregate sum and spread on nested relation', async () => { + const res = await selectQueries.selectWithAggregateSumAndSpreadOnNestedRelation.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "messages": Array [ + Object { + "channels": Object { + "details": "Details for public channel", + "details_sum": 1, + "sum": 1, + }, + }, + Object { + "channels": Object { + "details": "Details for random channel", + "details_sum": 2, + "sum": 2, + }, + }, + ], + "username": "supabot", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('select with type casting query', async () => { + const res = await selectQueries.typeCastingQuery.limit(1).single() + + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "id": "1", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +})