From b74466ba424fdf233ed04563209f7268ccb8912f Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 30 Oct 2024 00:02:31 +0100 Subject: [PATCH 01/31] fix(types): spread and embeding combination issues --- src/select-query-parser/result.ts | 22 +- test/select-query-parser/result.test-d.ts | 16 + test/select-query-parser/test-d.ts | 1130 +++++++++++++++++++++ 3 files changed, 1158 insertions(+), 10 deletions(-) create mode 100644 test/select-query-parser/test-d.ts diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index 0384389c..3327f5f2 100644 --- a/src/select-query-parser/result.ts +++ b/src/select-query-parser/result.ts @@ -130,11 +130,11 @@ export type ProcessRPCNode< Row extends Record, RelationName extends string, NodeType extends Ast.Node -> = NodeType extends Ast.StarNode // If the selection is * +> = NodeType['type'] extends Ast.StarNode['type'] // If the selection is * ? Row - : NodeType extends Ast.FieldNode - ? ProcessSimpleField - : SelectQueryError<'Unsupported node type.'> + : NodeType['type'] extends Ast.FieldNode['type'] + ? ProcessSimpleField> + : SelectQueryError<'RPC Unsupported node type.'> /** * Process select call that can be chained after an rpc call */ @@ -197,12 +197,12 @@ export type ProcessNode< RelationName extends string, Relationships extends GenericRelationship[], NodeType extends Ast.Node -> = NodeType extends Ast.StarNode // If the selection is * +> = NodeType['type'] extends Ast.StarNode['type'] // If the selection is * ? Row - : NodeType extends Ast.SpreadNode // If the selection is a ...spread - ? ProcessSpreadNode - : NodeType extends Ast.FieldNode - ? ProcessFieldNode + : NodeType['type'] extends Ast.SpreadNode['type'] // If the selection is a ...spread + ? ProcessSpreadNode> + : NodeType['type'] extends Ast.FieldNode['type'] + ? ProcessFieldNode> : SelectQueryError<'Unsupported node type.'> /** @@ -369,7 +369,9 @@ type ProcessSpreadNode< /** * Helper type to process the result of a spread node. */ -type ProcessSpreadNodeResult = ExtractFirstProperty extends infer SpreadedObject +type ProcessSpreadNodeResult = Result extends Record> + ? Result + : ExtractFirstProperty extends infer SpreadedObject ? ContainsNull extends true ? Exclude<{ [K in keyof SpreadedObject]: SpreadedObject[K] | null }, null> : Exclude<{ [K in keyof SpreadedObject]: SpreadedObject[K] }, null> diff --git a/test/select-query-parser/result.test-d.ts b/test/select-query-parser/result.test-d.ts index 38dcccae..d883cd32 100644 --- a/test/select-query-parser/result.test-d.ts +++ b/test/select-query-parser/result.test-d.ts @@ -3,6 +3,7 @@ import { selectParams } from '../relationships' import { GetResult } from '../../src/select-query-parser/result' import { expectType } from 'tsd' import { TypeEqual } from 'ts-expect' +import { SelectQueryError } from '../../src/select-query-parser/utils' type SelectQueryFromTableResult< TableName extends keyof Database['public']['Tables'], @@ -70,3 +71,18 @@ type SelectQueryFromTableResult< } expectType>(true) } + +// nested query with selective fields +{ + let result: SelectQueryFromTableResult< + 'users', + `msgs:messages(id, ...message_details(created_at, channel!inner(id, slug, owner:users(*))))` + > + let expected: { + msgs: { + id: number + message_details: SelectQueryError<`Could not embed because more than one relationship was found for 'messages' and '${string}' you need to hint the column with messages! ?`> + }[] + } + expectType>(true) +} diff --git a/test/select-query-parser/test-d.ts b/test/select-query-parser/test-d.ts new file mode 100644 index 00000000..202591d9 --- /dev/null +++ b/test/select-query-parser/test-d.ts @@ -0,0 +1,1130 @@ +import { PostgrestClient } from '../../src/index' +import { expectType } from 'tsd' +import { TypeEqual } from 'ts-expect' + +export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[] + +export type Database = { + graphql_public: { + Tables: { + [_ in never]: never + } + Views: { + [_ in never]: never + } + Functions: { + graphql: { + Args: { + operationName?: string + query?: string + variables?: Json + extensions?: Json + } + Returns: Json + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + pgbouncer: { + Tables: { + [_ in never]: never + } + Views: { + [_ in never]: never + } + Functions: { + get_auth: { + Args: { + p_usename: string + } + Returns: { + username: string + password: string + }[] + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + public: { + Tables: { + drop_item: { + Row: { + drop_item_data_id: number | null + id: number + item_id: number + item_name: string + rate: number | null + stack: number + } + Insert: { + drop_item_data_id?: number | null + id?: number + item_id: number + item_name?: string + rate?: number | null + stack?: number + } + Update: { + drop_item_data_id?: number | null + id?: number + item_id?: number + item_name?: string + rate?: number | null + stack?: number + } + Relationships: [ + { + foreignKeyName: 'drop_item_drop_item_data_id_fkey' + columns: ['drop_item_data_id'] + referencedRelation: 'drop_item_data' + referencedColumns: ['id'] + }, + { + foreignKeyName: 'drop_item_item_id_fkey' + columns: ['item_id'] + referencedRelation: 'item' + referencedColumns: ['id'] + } + ] + } + drop_item_data: { + Row: { + avg_gold: number | null + blue_rate: number | null + drop_gold_rate: number | null + green_rate: number | null + id: number + level: number | null + monster_id: number + name: string + not_drop_rate: number | null + orange_rate: number | null + rand_gold: number | null + rand_times: number + unknown_string: string | null + unknown1: number | null + yellow_rate: number | null + } + Insert: { + avg_gold?: number | null + blue_rate?: number | null + drop_gold_rate?: number | null + green_rate?: number | null + id?: number + level?: number | null + monster_id: number + name: string + not_drop_rate?: number | null + orange_rate?: number | null + rand_gold?: number | null + rand_times?: number + unknown_string?: string | null + unknown1?: number | null + yellow_rate?: number | null + } + Update: { + avg_gold?: number | null + blue_rate?: number | null + drop_gold_rate?: number | null + green_rate?: number | null + id?: number + level?: number | null + monster_id?: number + name?: string + not_drop_rate?: number | null + orange_rate?: number | null + rand_gold?: number | null + rand_times?: number + unknown_string?: string | null + unknown1?: number | null + yellow_rate?: number | null + } + Relationships: [ + { + foreignKeyName: 'drop_item_data_monster_id_fkey' + columns: ['monster_id'] + referencedRelation: 'monster' + referencedColumns: ['id'] + } + ] + } + item: { + Row: { + agi: number | null + attack: number | null + attack_range: number | null + attack_speed: number | null + attribute: number | null + attribute_damage: number | null + attribute_rate: number | null + attribute_resist: number | null + auction_type: number | null + avg_physical_damage: number | null + backpack_size: number | null + block_rate: number | null + casting_time: number | null + cooldown_group: number | null + cooldown_time: number | null + dodge_rate: number | null + drop_index: number | null + drop_rate: number | null + due_date_time: number | null + elf_skill_id: number | null + enchant_duration: number | null + enchant_id: number | null + enchant_time_type: number | null + enchant_type: number | null + enhance_effect_id: number | null + equip_type: number | null + expert_enchant_id: number | null + expert_level: number | null + extra_data1: number | null + extra_data2: number | null + extra_data3: number | null + fly_effect_id: number | null + hit_rate: number | null + icon_filename: string | null + id: number + int: number | null + item_group: string | null + item_quality: string | null + item_type: number | null + limit_type: number | null + log_level: number | null + magic_critical_damage: number | null + magic_critical_rate: number | null + magic_damage: number | null + magic_defence: number | null + magic_penetration: number | null + magic_penetration_defence: number | null + max_durability: number | null + max_hp: number | null + max_mp: number | null + max_socket: number | null + max_stack: number | null + model_filename: string | null + model_id: string | null + name: string | null + name_translation_id: string + op_flags: number | null + op_flags_plus: number | null + physical_critical_damage: number | null + physical_critical_rate: number | null + physical_defence: number | null + physical_penetration: number | null + physical_penetration_defence: number | null + rand_physical_damage: number | null + range_attack: number | null + rebirth_count: number | null + rebirth_max_score: number | null + rebirth_score: number | null + restrict_align: number | null + restrict_class: string | null + restrict_event_pos_id: string | null + restrict_gender: number | null + restrict_level: number + restrict_max_level: number | null + restrict_prestige: number | null + shop_price_type: number | null + socket_rate: number | null + special_damage: number | null + special_rate: number | null + special_type: number | null + str: number | null + sys_price: number | null + target: number | null + tip: string | null + tip_translation_id: string + treasure_buffs1: number | null + treasure_buffs2: number | null + treasure_buffs3: number | null + treasure_buffs4: number | null + unknown_string: string | null + used_effect_id: number | null + used_sound_name: string | null + vit: number | null + weapon_effect_id: number | null + will: number | null + } + Insert: { + agi?: number | null + attack?: number | null + attack_range?: number | null + attack_speed?: number | null + attribute?: number | null + attribute_damage?: number | null + attribute_rate?: number | null + attribute_resist?: number | null + auction_type?: number | null + avg_physical_damage?: number | null + backpack_size?: number | null + block_rate?: number | null + casting_time?: number | null + cooldown_group?: number | null + cooldown_time?: number | null + dodge_rate?: number | null + drop_index?: number | null + drop_rate?: number | null + due_date_time?: number | null + elf_skill_id?: number | null + enchant_duration?: number | null + enchant_id?: number | null + enchant_time_type?: number | null + enchant_type?: number | null + enhance_effect_id?: number | null + equip_type?: number | null + expert_enchant_id?: number | null + expert_level?: number | null + extra_data1?: number | null + extra_data2?: number | null + extra_data3?: number | null + fly_effect_id?: number | null + hit_rate?: number | null + icon_filename?: string | null + id: number + int?: number | null + item_group?: string | null + item_quality?: string | null + item_type?: number | null + limit_type?: number | null + log_level?: number | null + magic_critical_damage?: number | null + magic_critical_rate?: number | null + magic_damage?: number | null + magic_defence?: number | null + magic_penetration?: number | null + magic_penetration_defence?: number | null + max_durability?: number | null + max_hp?: number | null + max_mp?: number | null + max_socket?: number | null + max_stack?: number | null + model_filename?: string | null + model_id?: string | null + name?: string | null + name_translation_id: string + op_flags?: number | null + op_flags_plus?: number | null + physical_critical_damage?: number | null + physical_critical_rate?: number | null + physical_defence?: number | null + physical_penetration?: number | null + physical_penetration_defence?: number | null + rand_physical_damage?: number | null + range_attack?: number | null + rebirth_count?: number | null + rebirth_max_score?: number | null + rebirth_score?: number | null + restrict_align?: number | null + restrict_class?: string | null + restrict_event_pos_id?: string | null + restrict_gender?: number | null + restrict_level?: number + restrict_max_level?: number | null + restrict_prestige?: number | null + shop_price_type?: number | null + socket_rate?: number | null + special_damage?: number | null + special_rate?: number | null + special_type?: number | null + str?: number | null + sys_price?: number | null + target?: number | null + tip?: string | null + tip_translation_id: string + treasure_buffs1?: number | null + treasure_buffs2?: number | null + treasure_buffs3?: number | null + treasure_buffs4?: number | null + unknown_string?: string | null + used_effect_id?: number | null + used_sound_name?: string | null + vit?: number | null + weapon_effect_id?: number | null + will?: number | null + } + Update: { + agi?: number | null + attack?: number | null + attack_range?: number | null + attack_speed?: number | null + attribute?: number | null + attribute_damage?: number | null + attribute_rate?: number | null + attribute_resist?: number | null + auction_type?: number | null + avg_physical_damage?: number | null + backpack_size?: number | null + block_rate?: number | null + casting_time?: number | null + cooldown_group?: number | null + cooldown_time?: number | null + dodge_rate?: number | null + drop_index?: number | null + drop_rate?: number | null + due_date_time?: number | null + elf_skill_id?: number | null + enchant_duration?: number | null + enchant_id?: number | null + enchant_time_type?: number | null + enchant_type?: number | null + enhance_effect_id?: number | null + equip_type?: number | null + expert_enchant_id?: number | null + expert_level?: number | null + extra_data1?: number | null + extra_data2?: number | null + extra_data3?: number | null + fly_effect_id?: number | null + hit_rate?: number | null + icon_filename?: string | null + id?: number + int?: number | null + item_group?: string | null + item_quality?: string | null + item_type?: number | null + limit_type?: number | null + log_level?: number | null + magic_critical_damage?: number | null + magic_critical_rate?: number | null + magic_damage?: number | null + magic_defence?: number | null + magic_penetration?: number | null + magic_penetration_defence?: number | null + max_durability?: number | null + max_hp?: number | null + max_mp?: number | null + max_socket?: number | null + max_stack?: number | null + model_filename?: string | null + model_id?: string | null + name?: string | null + name_translation_id?: string + op_flags?: number | null + op_flags_plus?: number | null + physical_critical_damage?: number | null + physical_critical_rate?: number | null + physical_defence?: number | null + physical_penetration?: number | null + physical_penetration_defence?: number | null + rand_physical_damage?: number | null + range_attack?: number | null + rebirth_count?: number | null + rebirth_max_score?: number | null + rebirth_score?: number | null + restrict_align?: number | null + restrict_class?: string | null + restrict_event_pos_id?: string | null + restrict_gender?: number | null + restrict_level?: number + restrict_max_level?: number | null + restrict_prestige?: number | null + shop_price_type?: number | null + socket_rate?: number | null + special_damage?: number | null + special_rate?: number | null + special_type?: number | null + str?: number | null + sys_price?: number | null + target?: number | null + tip?: string | null + tip_translation_id?: string + treasure_buffs1?: number | null + treasure_buffs2?: number | null + treasure_buffs3?: number | null + treasure_buffs4?: number | null + unknown_string?: string | null + used_effect_id?: number | null + used_sound_name?: string | null + vit?: number | null + weapon_effect_id?: number | null + will?: number | null + } + Relationships: [ + { + foreignKeyName: 'item_name_translation_id_fkey' + columns: ['name_translation_id'] + referencedRelation: 'translation' + referencedColumns: ['id'] + }, + { + foreignKeyName: 'item_tip_translation_id_fkey' + columns: ['tip_translation_id'] + referencedRelation: 'translation' + referencedColumns: ['id'] + } + ] + } + monster: { + Row: { + achievement_map: string | null + ai_id: number | null + attack: number | null + attack_range: number | null + attack_speed: number | null + attribute: number | null + attribute_damage: number | null + attribute_rate: number | null + attribute_resist: number | null + auto_spell_during: boolean | null + auto_spell_id: number | null + avg_physical_damage: number | null + battle_spells: string | null + casting_effect_id: number | null + detect_range: number | null + dodge_rate: number | null + exp: number | null + fear_type: number | null + hit_rate: number | null + id: number + idle_spell_id: number | null + innate_buffs: string | null + level: number + locate_limit: number | null + lower_limit: number | null + magic_critical_damage: number | null + magic_critical_rate: number | null + magic_damage: number | null + magic_defence: number | null + magical_penetration: number | null + magical_penetration_defence: number | null + max_call_help: number | null + max_hp: number | null + max_mp: number | null + model_ids: string | null + model_scales: number | null + monster_alignment: number | null + move_range: number | null + name: string | null + name_translation_id: string + part_breaking_action: number | null + part_hp: number | null + physical_critical_damage: number | null + physical_critical_rate: number | null + physical_defence: number | null + physical_penetration: number | null + physical_penetration_defence: number | null + pursuit_speed: number | null + rand_buff_num: number | null + rand_physical_damage: number | null + random_buffs: string | null + rank: number | null + restore_spell_id: number | null + roam_speed: number | null + shout: string | null + shout_for_man: string | null + shout_for_woman: string | null + special_flag: number | null + spell_shout_cmds: string | null + summon_effect_id: number | null + summon_max: number | null + summon_monster_id: number | null + summon_rate: number | null + summon_type: number | null + type: number | null + zone_icon: string | null + } + Insert: { + achievement_map?: string | null + ai_id?: number | null + attack?: number | null + attack_range?: number | null + attack_speed?: number | null + attribute?: number | null + attribute_damage?: number | null + attribute_rate?: number | null + attribute_resist?: number | null + auto_spell_during?: boolean | null + auto_spell_id?: number | null + avg_physical_damage?: number | null + battle_spells?: string | null + casting_effect_id?: number | null + detect_range?: number | null + dodge_rate?: number | null + exp?: number | null + fear_type?: number | null + hit_rate?: number | null + id?: number + idle_spell_id?: number | null + innate_buffs?: string | null + level?: number + locate_limit?: number | null + lower_limit?: number | null + magic_critical_damage?: number | null + magic_critical_rate?: number | null + magic_damage?: number | null + magic_defence?: number | null + magical_penetration?: number | null + magical_penetration_defence?: number | null + max_call_help?: number | null + max_hp?: number | null + max_mp?: number | null + model_ids?: string | null + model_scales?: number | null + monster_alignment?: number | null + move_range?: number | null + name?: string | null + name_translation_id: string + part_breaking_action?: number | null + part_hp?: number | null + physical_critical_damage?: number | null + physical_critical_rate?: number | null + physical_defence?: number | null + physical_penetration?: number | null + physical_penetration_defence?: number | null + pursuit_speed?: number | null + rand_buff_num?: number | null + rand_physical_damage?: number | null + random_buffs?: string | null + rank?: number | null + restore_spell_id?: number | null + roam_speed?: number | null + shout?: string | null + shout_for_man?: string | null + shout_for_woman?: string | null + special_flag?: number | null + spell_shout_cmds?: string | null + summon_effect_id?: number | null + summon_max?: number | null + summon_monster_id?: number | null + summon_rate?: number | null + summon_type?: number | null + type?: number | null + zone_icon?: string | null + } + Update: { + achievement_map?: string | null + ai_id?: number | null + attack?: number | null + attack_range?: number | null + attack_speed?: number | null + attribute?: number | null + attribute_damage?: number | null + attribute_rate?: number | null + attribute_resist?: number | null + auto_spell_during?: boolean | null + auto_spell_id?: number | null + avg_physical_damage?: number | null + battle_spells?: string | null + casting_effect_id?: number | null + detect_range?: number | null + dodge_rate?: number | null + exp?: number | null + fear_type?: number | null + hit_rate?: number | null + id?: number + idle_spell_id?: number | null + innate_buffs?: string | null + level?: number + locate_limit?: number | null + lower_limit?: number | null + magic_critical_damage?: number | null + magic_critical_rate?: number | null + magic_damage?: number | null + magic_defence?: number | null + magical_penetration?: number | null + magical_penetration_defence?: number | null + max_call_help?: number | null + max_hp?: number | null + max_mp?: number | null + model_ids?: string | null + model_scales?: number | null + monster_alignment?: number | null + move_range?: number | null + name?: string | null + name_translation_id?: string + part_breaking_action?: number | null + part_hp?: number | null + physical_critical_damage?: number | null + physical_critical_rate?: number | null + physical_defence?: number | null + physical_penetration?: number | null + physical_penetration_defence?: number | null + pursuit_speed?: number | null + rand_buff_num?: number | null + rand_physical_damage?: number | null + random_buffs?: string | null + rank?: number | null + restore_spell_id?: number | null + roam_speed?: number | null + shout?: string | null + shout_for_man?: string | null + shout_for_woman?: string | null + special_flag?: number | null + spell_shout_cmds?: string | null + summon_effect_id?: number | null + summon_max?: number | null + summon_monster_id?: number | null + summon_rate?: number | null + summon_type?: number | null + type?: number | null + zone_icon?: string | null + } + Relationships: [ + { + foreignKeyName: 'monster_name_translation_id_fkey' + columns: ['name_translation_id'] + referencedRelation: 'translation' + referencedColumns: ['id'] + } + ] + } + translation: { + Row: { + en: string + es: string + fr: string + id: string + pt: string + } + Insert: { + en?: string + es?: string + fr?: string + id: string + pt?: string + } + Update: { + en?: string + es?: string + fr?: string + id?: string + pt?: string + } + Relationships: [] + } + } + Views: { + [_ in never]: never + } + Functions: { + check_bitwise_and_hex: { + Args: { + restrict_class_hex: string + bitmask: number + } + Returns: boolean + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + storage: { + Tables: { + buckets: { + Row: { + allowed_mime_types: string[] | null + avif_autodetection: boolean | null + created_at: string | null + file_size_limit: number | null + id: string + name: string + owner: string | null + owner_id: string | null + public: boolean | null + updated_at: string | null + } + Insert: { + allowed_mime_types?: string[] | null + avif_autodetection?: boolean | null + created_at?: string | null + file_size_limit?: number | null + id: string + name: string + owner?: string | null + owner_id?: string | null + public?: boolean | null + updated_at?: string | null + } + Update: { + allowed_mime_types?: string[] | null + avif_autodetection?: boolean | null + created_at?: string | null + file_size_limit?: number | null + id?: string + name?: string + owner?: string | null + owner_id?: string | null + public?: boolean | null + updated_at?: string | null + } + Relationships: [] + } + migrations: { + Row: { + executed_at: string | null + hash: string + id: number + name: string + } + Insert: { + executed_at?: string | null + hash: string + id: number + name: string + } + Update: { + executed_at?: string | null + hash?: string + id?: number + name?: string + } + Relationships: [] + } + objects: { + Row: { + bucket_id: string | null + created_at: string | null + id: string + last_accessed_at: string | null + metadata: Json | null + name: string | null + owner: string | null + owner_id: string | null + path_tokens: string[] | null + updated_at: string | null + version: string | null + } + Insert: { + bucket_id?: string | null + created_at?: string | null + id?: string + last_accessed_at?: string | null + metadata?: Json | null + name?: string | null + owner?: string | null + owner_id?: string | null + path_tokens?: string[] | null + updated_at?: string | null + version?: string | null + } + Update: { + bucket_id?: string | null + created_at?: string | null + id?: string + last_accessed_at?: string | null + metadata?: Json | null + name?: string | null + owner?: string | null + owner_id?: string | null + path_tokens?: string[] | null + updated_at?: string | null + version?: string | null + } + Relationships: [ + { + foreignKeyName: 'objects_bucketId_fkey' + columns: ['bucket_id'] + referencedRelation: 'buckets' + referencedColumns: ['id'] + } + ] + } + s3_multipart_uploads: { + Row: { + bucket_id: string + created_at: string + id: string + in_progress_size: number + key: string + owner_id: string | null + upload_signature: string + version: string + } + Insert: { + bucket_id: string + created_at?: string + id: string + in_progress_size?: number + key: string + owner_id?: string | null + upload_signature: string + version: string + } + Update: { + bucket_id?: string + created_at?: string + id?: string + in_progress_size?: number + key?: string + owner_id?: string | null + upload_signature?: string + version?: string + } + Relationships: [ + { + foreignKeyName: 's3_multipart_uploads_bucket_id_fkey' + columns: ['bucket_id'] + referencedRelation: 'buckets' + referencedColumns: ['id'] + } + ] + } + s3_multipart_uploads_parts: { + Row: { + bucket_id: string + created_at: string + etag: string + id: string + key: string + owner_id: string | null + part_number: number + size: number + upload_id: string + version: string + } + Insert: { + bucket_id: string + created_at?: string + etag: string + id?: string + key: string + owner_id?: string | null + part_number: number + size?: number + upload_id: string + version: string + } + Update: { + bucket_id?: string + created_at?: string + etag?: string + id?: string + key?: string + owner_id?: string | null + part_number?: number + size?: number + upload_id?: string + version?: string + } + Relationships: [ + { + foreignKeyName: 's3_multipart_uploads_parts_bucket_id_fkey' + columns: ['bucket_id'] + referencedRelation: 'buckets' + referencedColumns: ['id'] + }, + { + foreignKeyName: 's3_multipart_uploads_parts_upload_id_fkey' + columns: ['upload_id'] + referencedRelation: 's3_multipart_uploads' + referencedColumns: ['id'] + } + ] + } + } + Views: { + [_ in never]: never + } + Functions: { + can_insert_object: { + Args: { + bucketid: string + name: string + owner: string + metadata: Json + } + Returns: undefined + } + extension: { + Args: { + name: string + } + Returns: string + } + filename: { + Args: { + name: string + } + Returns: string + } + foldername: { + Args: { + name: string + } + Returns: string[] + } + get_size_by_bucket: { + Args: Record + Returns: { + size: number + bucket_id: string + }[] + } + list_multipart_uploads_with_delimiter: { + Args: { + bucket_id: string + prefix_param: string + delimiter_param: string + max_keys?: number + next_key_token?: string + next_upload_token?: string + } + Returns: { + key: string + id: string + created_at: string + }[] + } + list_objects_with_delimiter: { + Args: { + bucket_id: string + prefix_param: string + delimiter_param: string + max_keys?: number + start_after?: string + next_token?: string + } + Returns: { + name: string + id: string + metadata: Json + updated_at: string + }[] + } + search: { + Args: { + prefix: string + bucketname: string + limits?: number + levels?: number + offsets?: number + search?: string + sortcolumn?: string + sortorder?: string + } + Returns: { + name: string + id: string + updated_at: string + created_at: string + last_accessed_at: string + metadata: Json + }[] + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } +} + +type PublicSchema = Database[Extract] + +export type Tables< + PublicTableNameOrOptions extends + | keyof (PublicSchema['Tables'] & PublicSchema['Views']) + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof (Database[PublicTableNameOrOptions['schema']]['Tables'] & + Database[PublicTableNameOrOptions['schema']]['Views']) + : never = never +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? (Database[PublicTableNameOrOptions['schema']]['Tables'] & + Database[PublicTableNameOrOptions['schema']]['Views'])[TableName] extends { + Row: infer R + } + ? R + : never + : PublicTableNameOrOptions extends keyof (PublicSchema['Tables'] & PublicSchema['Views']) + ? (PublicSchema['Tables'] & PublicSchema['Views'])[PublicTableNameOrOptions] extends { + Row: infer R + } + ? R + : never + : never + +export type TablesInsert< + PublicTableNameOrOptions extends keyof PublicSchema['Tables'] | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions['schema']]['Tables'] + : never = never +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends { + Insert: infer I + } + ? I + : never + : PublicTableNameOrOptions extends keyof PublicSchema['Tables'] + ? PublicSchema['Tables'][PublicTableNameOrOptions] extends { + Insert: infer I + } + ? I + : never + : never + +export type TablesUpdate< + PublicTableNameOrOptions extends keyof PublicSchema['Tables'] | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions['schema']]['Tables'] + : never = never +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends { + Update: infer U + } + ? U + : never + : PublicTableNameOrOptions extends keyof PublicSchema['Tables'] + ? PublicSchema['Tables'][PublicTableNameOrOptions] extends { + Update: infer U + } + ? U + : never + : never + +export type Enums< + PublicEnumNameOrOptions extends keyof PublicSchema['Enums'] | { schema: keyof Database }, + EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicEnumNameOrOptions['schema']]['Enums'] + : never = never +> = PublicEnumNameOrOptions extends { schema: keyof Database } + ? Database[PublicEnumNameOrOptions['schema']]['Enums'][EnumName] + : PublicEnumNameOrOptions extends keyof PublicSchema['Enums'] + ? PublicSchema['Enums'][PublicEnumNameOrOptions] + : never + +const REST_URL = 'http://localhost:3000' +export const postgrest = new PostgrestClient(REST_URL) + +// nested query with selective fields +{ + const { data } = await postgrest + .from('item') + .select( + `drops:drop_item(...drop_item_data(rand_times, monster!inner(id, level, name:translation(*))))` + ) + .limit(1) + .single() + let result: Exclude + let expected: { + name: { + en: string + es: string + fr: string + id: string + pt: string + } + id: number + level: number + } | null + type drops = (typeof result.drops)[0] + type monster = drops['monster'] + expectType>(true) +} From c995e4c1e607079e60cbcb14b705670413876f32 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 30 Oct 2024 00:03:52 +0100 Subject: [PATCH 02/31] chore: remove debug test case --- test/select-query-parser/test-d.ts | 1130 ---------------------------- 1 file changed, 1130 deletions(-) delete mode 100644 test/select-query-parser/test-d.ts diff --git a/test/select-query-parser/test-d.ts b/test/select-query-parser/test-d.ts deleted file mode 100644 index 202591d9..00000000 --- a/test/select-query-parser/test-d.ts +++ /dev/null @@ -1,1130 +0,0 @@ -import { PostgrestClient } from '../../src/index' -import { expectType } from 'tsd' -import { TypeEqual } from 'ts-expect' - -export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[] - -export type Database = { - graphql_public: { - Tables: { - [_ in never]: never - } - Views: { - [_ in never]: never - } - Functions: { - graphql: { - Args: { - operationName?: string - query?: string - variables?: Json - extensions?: Json - } - Returns: Json - } - } - Enums: { - [_ in never]: never - } - CompositeTypes: { - [_ in never]: never - } - } - pgbouncer: { - Tables: { - [_ in never]: never - } - Views: { - [_ in never]: never - } - Functions: { - get_auth: { - Args: { - p_usename: string - } - Returns: { - username: string - password: string - }[] - } - } - Enums: { - [_ in never]: never - } - CompositeTypes: { - [_ in never]: never - } - } - public: { - Tables: { - drop_item: { - Row: { - drop_item_data_id: number | null - id: number - item_id: number - item_name: string - rate: number | null - stack: number - } - Insert: { - drop_item_data_id?: number | null - id?: number - item_id: number - item_name?: string - rate?: number | null - stack?: number - } - Update: { - drop_item_data_id?: number | null - id?: number - item_id?: number - item_name?: string - rate?: number | null - stack?: number - } - Relationships: [ - { - foreignKeyName: 'drop_item_drop_item_data_id_fkey' - columns: ['drop_item_data_id'] - referencedRelation: 'drop_item_data' - referencedColumns: ['id'] - }, - { - foreignKeyName: 'drop_item_item_id_fkey' - columns: ['item_id'] - referencedRelation: 'item' - referencedColumns: ['id'] - } - ] - } - drop_item_data: { - Row: { - avg_gold: number | null - blue_rate: number | null - drop_gold_rate: number | null - green_rate: number | null - id: number - level: number | null - monster_id: number - name: string - not_drop_rate: number | null - orange_rate: number | null - rand_gold: number | null - rand_times: number - unknown_string: string | null - unknown1: number | null - yellow_rate: number | null - } - Insert: { - avg_gold?: number | null - blue_rate?: number | null - drop_gold_rate?: number | null - green_rate?: number | null - id?: number - level?: number | null - monster_id: number - name: string - not_drop_rate?: number | null - orange_rate?: number | null - rand_gold?: number | null - rand_times?: number - unknown_string?: string | null - unknown1?: number | null - yellow_rate?: number | null - } - Update: { - avg_gold?: number | null - blue_rate?: number | null - drop_gold_rate?: number | null - green_rate?: number | null - id?: number - level?: number | null - monster_id?: number - name?: string - not_drop_rate?: number | null - orange_rate?: number | null - rand_gold?: number | null - rand_times?: number - unknown_string?: string | null - unknown1?: number | null - yellow_rate?: number | null - } - Relationships: [ - { - foreignKeyName: 'drop_item_data_monster_id_fkey' - columns: ['monster_id'] - referencedRelation: 'monster' - referencedColumns: ['id'] - } - ] - } - item: { - Row: { - agi: number | null - attack: number | null - attack_range: number | null - attack_speed: number | null - attribute: number | null - attribute_damage: number | null - attribute_rate: number | null - attribute_resist: number | null - auction_type: number | null - avg_physical_damage: number | null - backpack_size: number | null - block_rate: number | null - casting_time: number | null - cooldown_group: number | null - cooldown_time: number | null - dodge_rate: number | null - drop_index: number | null - drop_rate: number | null - due_date_time: number | null - elf_skill_id: number | null - enchant_duration: number | null - enchant_id: number | null - enchant_time_type: number | null - enchant_type: number | null - enhance_effect_id: number | null - equip_type: number | null - expert_enchant_id: number | null - expert_level: number | null - extra_data1: number | null - extra_data2: number | null - extra_data3: number | null - fly_effect_id: number | null - hit_rate: number | null - icon_filename: string | null - id: number - int: number | null - item_group: string | null - item_quality: string | null - item_type: number | null - limit_type: number | null - log_level: number | null - magic_critical_damage: number | null - magic_critical_rate: number | null - magic_damage: number | null - magic_defence: number | null - magic_penetration: number | null - magic_penetration_defence: number | null - max_durability: number | null - max_hp: number | null - max_mp: number | null - max_socket: number | null - max_stack: number | null - model_filename: string | null - model_id: string | null - name: string | null - name_translation_id: string - op_flags: number | null - op_flags_plus: number | null - physical_critical_damage: number | null - physical_critical_rate: number | null - physical_defence: number | null - physical_penetration: number | null - physical_penetration_defence: number | null - rand_physical_damage: number | null - range_attack: number | null - rebirth_count: number | null - rebirth_max_score: number | null - rebirth_score: number | null - restrict_align: number | null - restrict_class: string | null - restrict_event_pos_id: string | null - restrict_gender: number | null - restrict_level: number - restrict_max_level: number | null - restrict_prestige: number | null - shop_price_type: number | null - socket_rate: number | null - special_damage: number | null - special_rate: number | null - special_type: number | null - str: number | null - sys_price: number | null - target: number | null - tip: string | null - tip_translation_id: string - treasure_buffs1: number | null - treasure_buffs2: number | null - treasure_buffs3: number | null - treasure_buffs4: number | null - unknown_string: string | null - used_effect_id: number | null - used_sound_name: string | null - vit: number | null - weapon_effect_id: number | null - will: number | null - } - Insert: { - agi?: number | null - attack?: number | null - attack_range?: number | null - attack_speed?: number | null - attribute?: number | null - attribute_damage?: number | null - attribute_rate?: number | null - attribute_resist?: number | null - auction_type?: number | null - avg_physical_damage?: number | null - backpack_size?: number | null - block_rate?: number | null - casting_time?: number | null - cooldown_group?: number | null - cooldown_time?: number | null - dodge_rate?: number | null - drop_index?: number | null - drop_rate?: number | null - due_date_time?: number | null - elf_skill_id?: number | null - enchant_duration?: number | null - enchant_id?: number | null - enchant_time_type?: number | null - enchant_type?: number | null - enhance_effect_id?: number | null - equip_type?: number | null - expert_enchant_id?: number | null - expert_level?: number | null - extra_data1?: number | null - extra_data2?: number | null - extra_data3?: number | null - fly_effect_id?: number | null - hit_rate?: number | null - icon_filename?: string | null - id: number - int?: number | null - item_group?: string | null - item_quality?: string | null - item_type?: number | null - limit_type?: number | null - log_level?: number | null - magic_critical_damage?: number | null - magic_critical_rate?: number | null - magic_damage?: number | null - magic_defence?: number | null - magic_penetration?: number | null - magic_penetration_defence?: number | null - max_durability?: number | null - max_hp?: number | null - max_mp?: number | null - max_socket?: number | null - max_stack?: number | null - model_filename?: string | null - model_id?: string | null - name?: string | null - name_translation_id: string - op_flags?: number | null - op_flags_plus?: number | null - physical_critical_damage?: number | null - physical_critical_rate?: number | null - physical_defence?: number | null - physical_penetration?: number | null - physical_penetration_defence?: number | null - rand_physical_damage?: number | null - range_attack?: number | null - rebirth_count?: number | null - rebirth_max_score?: number | null - rebirth_score?: number | null - restrict_align?: number | null - restrict_class?: string | null - restrict_event_pos_id?: string | null - restrict_gender?: number | null - restrict_level?: number - restrict_max_level?: number | null - restrict_prestige?: number | null - shop_price_type?: number | null - socket_rate?: number | null - special_damage?: number | null - special_rate?: number | null - special_type?: number | null - str?: number | null - sys_price?: number | null - target?: number | null - tip?: string | null - tip_translation_id: string - treasure_buffs1?: number | null - treasure_buffs2?: number | null - treasure_buffs3?: number | null - treasure_buffs4?: number | null - unknown_string?: string | null - used_effect_id?: number | null - used_sound_name?: string | null - vit?: number | null - weapon_effect_id?: number | null - will?: number | null - } - Update: { - agi?: number | null - attack?: number | null - attack_range?: number | null - attack_speed?: number | null - attribute?: number | null - attribute_damage?: number | null - attribute_rate?: number | null - attribute_resist?: number | null - auction_type?: number | null - avg_physical_damage?: number | null - backpack_size?: number | null - block_rate?: number | null - casting_time?: number | null - cooldown_group?: number | null - cooldown_time?: number | null - dodge_rate?: number | null - drop_index?: number | null - drop_rate?: number | null - due_date_time?: number | null - elf_skill_id?: number | null - enchant_duration?: number | null - enchant_id?: number | null - enchant_time_type?: number | null - enchant_type?: number | null - enhance_effect_id?: number | null - equip_type?: number | null - expert_enchant_id?: number | null - expert_level?: number | null - extra_data1?: number | null - extra_data2?: number | null - extra_data3?: number | null - fly_effect_id?: number | null - hit_rate?: number | null - icon_filename?: string | null - id?: number - int?: number | null - item_group?: string | null - item_quality?: string | null - item_type?: number | null - limit_type?: number | null - log_level?: number | null - magic_critical_damage?: number | null - magic_critical_rate?: number | null - magic_damage?: number | null - magic_defence?: number | null - magic_penetration?: number | null - magic_penetration_defence?: number | null - max_durability?: number | null - max_hp?: number | null - max_mp?: number | null - max_socket?: number | null - max_stack?: number | null - model_filename?: string | null - model_id?: string | null - name?: string | null - name_translation_id?: string - op_flags?: number | null - op_flags_plus?: number | null - physical_critical_damage?: number | null - physical_critical_rate?: number | null - physical_defence?: number | null - physical_penetration?: number | null - physical_penetration_defence?: number | null - rand_physical_damage?: number | null - range_attack?: number | null - rebirth_count?: number | null - rebirth_max_score?: number | null - rebirth_score?: number | null - restrict_align?: number | null - restrict_class?: string | null - restrict_event_pos_id?: string | null - restrict_gender?: number | null - restrict_level?: number - restrict_max_level?: number | null - restrict_prestige?: number | null - shop_price_type?: number | null - socket_rate?: number | null - special_damage?: number | null - special_rate?: number | null - special_type?: number | null - str?: number | null - sys_price?: number | null - target?: number | null - tip?: string | null - tip_translation_id?: string - treasure_buffs1?: number | null - treasure_buffs2?: number | null - treasure_buffs3?: number | null - treasure_buffs4?: number | null - unknown_string?: string | null - used_effect_id?: number | null - used_sound_name?: string | null - vit?: number | null - weapon_effect_id?: number | null - will?: number | null - } - Relationships: [ - { - foreignKeyName: 'item_name_translation_id_fkey' - columns: ['name_translation_id'] - referencedRelation: 'translation' - referencedColumns: ['id'] - }, - { - foreignKeyName: 'item_tip_translation_id_fkey' - columns: ['tip_translation_id'] - referencedRelation: 'translation' - referencedColumns: ['id'] - } - ] - } - monster: { - Row: { - achievement_map: string | null - ai_id: number | null - attack: number | null - attack_range: number | null - attack_speed: number | null - attribute: number | null - attribute_damage: number | null - attribute_rate: number | null - attribute_resist: number | null - auto_spell_during: boolean | null - auto_spell_id: number | null - avg_physical_damage: number | null - battle_spells: string | null - casting_effect_id: number | null - detect_range: number | null - dodge_rate: number | null - exp: number | null - fear_type: number | null - hit_rate: number | null - id: number - idle_spell_id: number | null - innate_buffs: string | null - level: number - locate_limit: number | null - lower_limit: number | null - magic_critical_damage: number | null - magic_critical_rate: number | null - magic_damage: number | null - magic_defence: number | null - magical_penetration: number | null - magical_penetration_defence: number | null - max_call_help: number | null - max_hp: number | null - max_mp: number | null - model_ids: string | null - model_scales: number | null - monster_alignment: number | null - move_range: number | null - name: string | null - name_translation_id: string - part_breaking_action: number | null - part_hp: number | null - physical_critical_damage: number | null - physical_critical_rate: number | null - physical_defence: number | null - physical_penetration: number | null - physical_penetration_defence: number | null - pursuit_speed: number | null - rand_buff_num: number | null - rand_physical_damage: number | null - random_buffs: string | null - rank: number | null - restore_spell_id: number | null - roam_speed: number | null - shout: string | null - shout_for_man: string | null - shout_for_woman: string | null - special_flag: number | null - spell_shout_cmds: string | null - summon_effect_id: number | null - summon_max: number | null - summon_monster_id: number | null - summon_rate: number | null - summon_type: number | null - type: number | null - zone_icon: string | null - } - Insert: { - achievement_map?: string | null - ai_id?: number | null - attack?: number | null - attack_range?: number | null - attack_speed?: number | null - attribute?: number | null - attribute_damage?: number | null - attribute_rate?: number | null - attribute_resist?: number | null - auto_spell_during?: boolean | null - auto_spell_id?: number | null - avg_physical_damage?: number | null - battle_spells?: string | null - casting_effect_id?: number | null - detect_range?: number | null - dodge_rate?: number | null - exp?: number | null - fear_type?: number | null - hit_rate?: number | null - id?: number - idle_spell_id?: number | null - innate_buffs?: string | null - level?: number - locate_limit?: number | null - lower_limit?: number | null - magic_critical_damage?: number | null - magic_critical_rate?: number | null - magic_damage?: number | null - magic_defence?: number | null - magical_penetration?: number | null - magical_penetration_defence?: number | null - max_call_help?: number | null - max_hp?: number | null - max_mp?: number | null - model_ids?: string | null - model_scales?: number | null - monster_alignment?: number | null - move_range?: number | null - name?: string | null - name_translation_id: string - part_breaking_action?: number | null - part_hp?: number | null - physical_critical_damage?: number | null - physical_critical_rate?: number | null - physical_defence?: number | null - physical_penetration?: number | null - physical_penetration_defence?: number | null - pursuit_speed?: number | null - rand_buff_num?: number | null - rand_physical_damage?: number | null - random_buffs?: string | null - rank?: number | null - restore_spell_id?: number | null - roam_speed?: number | null - shout?: string | null - shout_for_man?: string | null - shout_for_woman?: string | null - special_flag?: number | null - spell_shout_cmds?: string | null - summon_effect_id?: number | null - summon_max?: number | null - summon_monster_id?: number | null - summon_rate?: number | null - summon_type?: number | null - type?: number | null - zone_icon?: string | null - } - Update: { - achievement_map?: string | null - ai_id?: number | null - attack?: number | null - attack_range?: number | null - attack_speed?: number | null - attribute?: number | null - attribute_damage?: number | null - attribute_rate?: number | null - attribute_resist?: number | null - auto_spell_during?: boolean | null - auto_spell_id?: number | null - avg_physical_damage?: number | null - battle_spells?: string | null - casting_effect_id?: number | null - detect_range?: number | null - dodge_rate?: number | null - exp?: number | null - fear_type?: number | null - hit_rate?: number | null - id?: number - idle_spell_id?: number | null - innate_buffs?: string | null - level?: number - locate_limit?: number | null - lower_limit?: number | null - magic_critical_damage?: number | null - magic_critical_rate?: number | null - magic_damage?: number | null - magic_defence?: number | null - magical_penetration?: number | null - magical_penetration_defence?: number | null - max_call_help?: number | null - max_hp?: number | null - max_mp?: number | null - model_ids?: string | null - model_scales?: number | null - monster_alignment?: number | null - move_range?: number | null - name?: string | null - name_translation_id?: string - part_breaking_action?: number | null - part_hp?: number | null - physical_critical_damage?: number | null - physical_critical_rate?: number | null - physical_defence?: number | null - physical_penetration?: number | null - physical_penetration_defence?: number | null - pursuit_speed?: number | null - rand_buff_num?: number | null - rand_physical_damage?: number | null - random_buffs?: string | null - rank?: number | null - restore_spell_id?: number | null - roam_speed?: number | null - shout?: string | null - shout_for_man?: string | null - shout_for_woman?: string | null - special_flag?: number | null - spell_shout_cmds?: string | null - summon_effect_id?: number | null - summon_max?: number | null - summon_monster_id?: number | null - summon_rate?: number | null - summon_type?: number | null - type?: number | null - zone_icon?: string | null - } - Relationships: [ - { - foreignKeyName: 'monster_name_translation_id_fkey' - columns: ['name_translation_id'] - referencedRelation: 'translation' - referencedColumns: ['id'] - } - ] - } - translation: { - Row: { - en: string - es: string - fr: string - id: string - pt: string - } - Insert: { - en?: string - es?: string - fr?: string - id: string - pt?: string - } - Update: { - en?: string - es?: string - fr?: string - id?: string - pt?: string - } - Relationships: [] - } - } - Views: { - [_ in never]: never - } - Functions: { - check_bitwise_and_hex: { - Args: { - restrict_class_hex: string - bitmask: number - } - Returns: boolean - } - } - Enums: { - [_ in never]: never - } - CompositeTypes: { - [_ in never]: never - } - } - storage: { - Tables: { - buckets: { - Row: { - allowed_mime_types: string[] | null - avif_autodetection: boolean | null - created_at: string | null - file_size_limit: number | null - id: string - name: string - owner: string | null - owner_id: string | null - public: boolean | null - updated_at: string | null - } - Insert: { - allowed_mime_types?: string[] | null - avif_autodetection?: boolean | null - created_at?: string | null - file_size_limit?: number | null - id: string - name: string - owner?: string | null - owner_id?: string | null - public?: boolean | null - updated_at?: string | null - } - Update: { - allowed_mime_types?: string[] | null - avif_autodetection?: boolean | null - created_at?: string | null - file_size_limit?: number | null - id?: string - name?: string - owner?: string | null - owner_id?: string | null - public?: boolean | null - updated_at?: string | null - } - Relationships: [] - } - migrations: { - Row: { - executed_at: string | null - hash: string - id: number - name: string - } - Insert: { - executed_at?: string | null - hash: string - id: number - name: string - } - Update: { - executed_at?: string | null - hash?: string - id?: number - name?: string - } - Relationships: [] - } - objects: { - Row: { - bucket_id: string | null - created_at: string | null - id: string - last_accessed_at: string | null - metadata: Json | null - name: string | null - owner: string | null - owner_id: string | null - path_tokens: string[] | null - updated_at: string | null - version: string | null - } - Insert: { - bucket_id?: string | null - created_at?: string | null - id?: string - last_accessed_at?: string | null - metadata?: Json | null - name?: string | null - owner?: string | null - owner_id?: string | null - path_tokens?: string[] | null - updated_at?: string | null - version?: string | null - } - Update: { - bucket_id?: string | null - created_at?: string | null - id?: string - last_accessed_at?: string | null - metadata?: Json | null - name?: string | null - owner?: string | null - owner_id?: string | null - path_tokens?: string[] | null - updated_at?: string | null - version?: string | null - } - Relationships: [ - { - foreignKeyName: 'objects_bucketId_fkey' - columns: ['bucket_id'] - referencedRelation: 'buckets' - referencedColumns: ['id'] - } - ] - } - s3_multipart_uploads: { - Row: { - bucket_id: string - created_at: string - id: string - in_progress_size: number - key: string - owner_id: string | null - upload_signature: string - version: string - } - Insert: { - bucket_id: string - created_at?: string - id: string - in_progress_size?: number - key: string - owner_id?: string | null - upload_signature: string - version: string - } - Update: { - bucket_id?: string - created_at?: string - id?: string - in_progress_size?: number - key?: string - owner_id?: string | null - upload_signature?: string - version?: string - } - Relationships: [ - { - foreignKeyName: 's3_multipart_uploads_bucket_id_fkey' - columns: ['bucket_id'] - referencedRelation: 'buckets' - referencedColumns: ['id'] - } - ] - } - s3_multipart_uploads_parts: { - Row: { - bucket_id: string - created_at: string - etag: string - id: string - key: string - owner_id: string | null - part_number: number - size: number - upload_id: string - version: string - } - Insert: { - bucket_id: string - created_at?: string - etag: string - id?: string - key: string - owner_id?: string | null - part_number: number - size?: number - upload_id: string - version: string - } - Update: { - bucket_id?: string - created_at?: string - etag?: string - id?: string - key?: string - owner_id?: string | null - part_number?: number - size?: number - upload_id?: string - version?: string - } - Relationships: [ - { - foreignKeyName: 's3_multipart_uploads_parts_bucket_id_fkey' - columns: ['bucket_id'] - referencedRelation: 'buckets' - referencedColumns: ['id'] - }, - { - foreignKeyName: 's3_multipart_uploads_parts_upload_id_fkey' - columns: ['upload_id'] - referencedRelation: 's3_multipart_uploads' - referencedColumns: ['id'] - } - ] - } - } - Views: { - [_ in never]: never - } - Functions: { - can_insert_object: { - Args: { - bucketid: string - name: string - owner: string - metadata: Json - } - Returns: undefined - } - extension: { - Args: { - name: string - } - Returns: string - } - filename: { - Args: { - name: string - } - Returns: string - } - foldername: { - Args: { - name: string - } - Returns: string[] - } - get_size_by_bucket: { - Args: Record - Returns: { - size: number - bucket_id: string - }[] - } - list_multipart_uploads_with_delimiter: { - Args: { - bucket_id: string - prefix_param: string - delimiter_param: string - max_keys?: number - next_key_token?: string - next_upload_token?: string - } - Returns: { - key: string - id: string - created_at: string - }[] - } - list_objects_with_delimiter: { - Args: { - bucket_id: string - prefix_param: string - delimiter_param: string - max_keys?: number - start_after?: string - next_token?: string - } - Returns: { - name: string - id: string - metadata: Json - updated_at: string - }[] - } - search: { - Args: { - prefix: string - bucketname: string - limits?: number - levels?: number - offsets?: number - search?: string - sortcolumn?: string - sortorder?: string - } - Returns: { - name: string - id: string - updated_at: string - created_at: string - last_accessed_at: string - metadata: Json - }[] - } - } - Enums: { - [_ in never]: never - } - CompositeTypes: { - [_ in never]: never - } - } -} - -type PublicSchema = Database[Extract] - -export type Tables< - PublicTableNameOrOptions extends - | keyof (PublicSchema['Tables'] & PublicSchema['Views']) - | { schema: keyof Database }, - TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof (Database[PublicTableNameOrOptions['schema']]['Tables'] & - Database[PublicTableNameOrOptions['schema']]['Views']) - : never = never -> = PublicTableNameOrOptions extends { schema: keyof Database } - ? (Database[PublicTableNameOrOptions['schema']]['Tables'] & - Database[PublicTableNameOrOptions['schema']]['Views'])[TableName] extends { - Row: infer R - } - ? R - : never - : PublicTableNameOrOptions extends keyof (PublicSchema['Tables'] & PublicSchema['Views']) - ? (PublicSchema['Tables'] & PublicSchema['Views'])[PublicTableNameOrOptions] extends { - Row: infer R - } - ? R - : never - : never - -export type TablesInsert< - PublicTableNameOrOptions extends keyof PublicSchema['Tables'] | { schema: keyof Database }, - TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof Database[PublicTableNameOrOptions['schema']]['Tables'] - : never = never -> = PublicTableNameOrOptions extends { schema: keyof Database } - ? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends { - Insert: infer I - } - ? I - : never - : PublicTableNameOrOptions extends keyof PublicSchema['Tables'] - ? PublicSchema['Tables'][PublicTableNameOrOptions] extends { - Insert: infer I - } - ? I - : never - : never - -export type TablesUpdate< - PublicTableNameOrOptions extends keyof PublicSchema['Tables'] | { schema: keyof Database }, - TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof Database[PublicTableNameOrOptions['schema']]['Tables'] - : never = never -> = PublicTableNameOrOptions extends { schema: keyof Database } - ? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends { - Update: infer U - } - ? U - : never - : PublicTableNameOrOptions extends keyof PublicSchema['Tables'] - ? PublicSchema['Tables'][PublicTableNameOrOptions] extends { - Update: infer U - } - ? U - : never - : never - -export type Enums< - PublicEnumNameOrOptions extends keyof PublicSchema['Enums'] | { schema: keyof Database }, - EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } - ? keyof Database[PublicEnumNameOrOptions['schema']]['Enums'] - : never = never -> = PublicEnumNameOrOptions extends { schema: keyof Database } - ? Database[PublicEnumNameOrOptions['schema']]['Enums'][EnumName] - : PublicEnumNameOrOptions extends keyof PublicSchema['Enums'] - ? PublicSchema['Enums'][PublicEnumNameOrOptions] - : never - -const REST_URL = 'http://localhost:3000' -export const postgrest = new PostgrestClient(REST_URL) - -// nested query with selective fields -{ - const { data } = await postgrest - .from('item') - .select( - `drops:drop_item(...drop_item_data(rand_times, monster!inner(id, level, name:translation(*))))` - ) - .limit(1) - .single() - let result: Exclude - let expected: { - name: { - en: string - es: string - fr: string - id: string - pt: string - } - id: number - level: number - } | null - type drops = (typeof result.drops)[0] - type monster = drops['monster'] - expectType>(true) -} From e7eb73b7ce6e54d522853f63852c5e92a642d7e0 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 30 Oct 2024 10:26:58 +0100 Subject: [PATCH 03/31] chore: reproduce invalid node type error --- src/select-query-parser/result.ts | 17 +- test/select-query-parser/result.test-d.ts | 32 ++++ test/select-query-parser/types.test-d.ts | 199 ---------------------- 3 files changed, 42 insertions(+), 206 deletions(-) delete mode 100644 test/select-query-parser/types.test-d.ts diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index 3327f5f2..cee0b150 100644 --- a/src/select-query-parser/result.ts +++ b/src/select-query-parser/result.ts @@ -197,13 +197,13 @@ export type ProcessNode< RelationName extends string, Relationships extends GenericRelationship[], NodeType extends Ast.Node -> = NodeType['type'] extends Ast.StarNode['type'] // If the selection is * +> = NodeType extends Ast.StarNode // If the selection is * ? Row - : NodeType['type'] extends Ast.SpreadNode['type'] // If the selection is a ...spread - ? ProcessSpreadNode> - : NodeType['type'] extends Ast.FieldNode['type'] - ? ProcessFieldNode> - : SelectQueryError<'Unsupported node type.'> + : NodeType extends Ast.SpreadNode // If the selection is a ...spread + ? ProcessSpreadNode + : NodeType extends Ast.FieldNode + ? ProcessFieldNode + : SelectQueryError<'Unsupported node type.' & { nodeType: NodeType }> /** * Processes a FieldNode and returns the resulting TypeScript type. @@ -369,7 +369,10 @@ type ProcessSpreadNode< /** * Helper type to process the result of a spread node. */ -type ProcessSpreadNodeResult = Result extends Record> +type ProcessSpreadNodeResult = Result extends Record< + string, + SelectQueryError | null +> ? Result : ExtractFirstProperty extends infer SpreadedObject ? ContainsNull extends true diff --git a/test/select-query-parser/result.test-d.ts b/test/select-query-parser/result.test-d.ts index d883cd32..ff841096 100644 --- a/test/select-query-parser/result.test-d.ts +++ b/test/select-query-parser/result.test-d.ts @@ -86,3 +86,35 @@ type SelectQueryFromTableResult< } expectType>(true) } + +{ + let result: SelectQueryFromTableResult< + 'users', + `msgs:messages(id, ...channels(id, ...channel_details(id, missing_col)))` + > + let expected: { + msgs: { + id: number + channel_details: SelectQueryError<"column 'missing_col' does not exist on 'channel_details'."> | null + }[] + } + expectType>(true) +} + +{ + let result1: SelectQueryFromTableResult< + 'users', + `msgs:messages(...channels(slug, channel_details!inner(id, details, channel:channels(*))))` + > + let result2: SelectQueryFromTableResult< + 'users', + `msgs:messages(...channels(slug, channel_details!inner(channel:channels(*), id, details)))` + > + let result3: SelectQueryFromTableResult< + 'users', + `msgs:messages(...channels!inner(slug, channel_details!inner(id, details, channel:channels(*))))` + > + // All variations should not change the result + expectType(result2!) + expectType(result3!) +} diff --git a/test/select-query-parser/types.test-d.ts b/test/select-query-parser/types.test-d.ts deleted file mode 100644 index 049630dd..00000000 --- a/test/select-query-parser/types.test-d.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { Database } from '../types' -import { selectParams } from '../relationships' -import { - ProcessEmbeddedResource, - ProcessNode, - ProcessNodes, -} from '../../src/select-query-parser/result' -import { expectType } from 'tsd' -import { TypeEqual } from 'ts-expect' -import { - FindMatchingTableRelationships, - IsRelationNullable, - FindJoinTableRelationship, -} from '../../src/select-query-parser/utils' -import { Json } from '../../src/select-query-parser/types' -import { ParseQuery } from '../../src/select-query-parser/parser' - -// This test file is here to ensure some of our helpers behave as expected for ease of development -// and debugging purposes - -// Searching for a relationship by direct foreignkey name -{ - let result: FindMatchingTableRelationships< - Database['public'], - Database['public']['Tables']['best_friends']['Relationships'], - 'best_friends_first_user_fkey' - > - let expected: { - foreignKeyName: 'best_friends_first_user_fkey' - columns: ['first_user'] - isOneToOne: false - referencedRelation: 'users' - referencedColumns: ['username'] - } & { match: 'fkname' } - expectType>(true) -} -// Searching for a relationship by column hoding the value reference -{ - let result: FindMatchingTableRelationships< - Database['public'], - Database['public']['Tables']['best_friends']['Relationships'], - 'first_user' - > - let expected: { - foreignKeyName: 'best_friends_first_user_fkey' - columns: ['first_user'] - isOneToOne: false - referencedRelation: 'users' - referencedColumns: ['username'] - } & { match: 'col' } - expectType>(true) -} -// should return the relation matching the "Tables" references -{ - let result: FindMatchingTableRelationships< - Database['public'], - Database['public']['Tables']['user_profiles']['Relationships'], - 'username' - > - let expected: { - foreignKeyName: 'user_profiles_username_fkey' - columns: ['username'] - isOneToOne: false - referencedRelation: 'users' - referencedColumns: ['username'] - } & { match: 'col' } - expectType>(true) -} -// Searching for a relationship by referenced table name -{ - let result: FindMatchingTableRelationships< - Database['public'], - Database['public']['Tables']['messages']['Relationships'], - 'users' - > - let expected: { - foreignKeyName: 'messages_username_fkey' - columns: ['username'] - isOneToOne: false - referencedRelation: 'users' - referencedColumns: ['username'] - } & { match: 'refrel' } - expectType>(true) -} -{ - let result: FindMatchingTableRelationships< - Database['public'], - Database['public']['Tables']['messages']['Relationships'], - 'channels' - > - let expected: { - foreignKeyName: 'messages_channel_id_fkey' - columns: ['channel_id'] - isOneToOne: false - referencedRelation: 'channels' - referencedColumns: ['id'] - } & { match: 'refrel' } - expectType>(true) -} - -// IsRelationNullable -{ - type BestFriendsTable = Database['public']['Tables']['best_friends'] - type NonNullableRelation = FindMatchingTableRelationships< - Database['public'], - BestFriendsTable['Relationships'], - 'best_friends_first_user_fkey' - > - type NullableRelation = FindMatchingTableRelationships< - Database['public'], - BestFriendsTable['Relationships'], - 'best_friends_third_wheel_fkey' - > - let nonNullableResult: IsRelationNullable - let nullableResult: IsRelationNullable - expectType(false) - expectType(true) -} - -// Test nodes relations crawling utils -{ - const { from, select } = selectParams.nestedQueryWithSelectiveFields - type Schema = Database['public'] - type RelationName = typeof from - type Row = Schema['Tables'][RelationName]['Row'] - type Relationships = Schema['Tables'][RelationName]['Relationships'] - type ParsedQuery = ParseQuery - type r1 = ProcessNode - expectType>(true) - type r2 = ProcessNodes - // fail because result for messages is ({id: string} | {message: string | null })[] - expectType< - TypeEqual - >(true) - type f3 = ParsedQuery[1] - type r3 = ProcessEmbeddedResource - expectType>(true) -} -// Select from the column holding the relation (0-1 relation) -{ - const { from, select } = selectParams.joinSelectViaColumn - type Schema = Database['public'] - type RelationName = typeof from - type Row = Schema['Tables'][RelationName]['Row'] - type Relationships = Schema['Tables'][RelationName]['Relationships'] - type ParsedQuery = ParseQuery - type r1 = ProcessNode - let expected: { - username: { - age_range: unknown | null - catchphrase: unknown | null - data: Json | null - status: Database['public']['Enums']['user_status'] | null - username: string - } | null - } - expectType(expected!) - type r2 = ProcessNodes - expectType(expected!) -} - -{ - type Schema = Database['public'] - type CurrentTableOrView = 'products' - type FieldName = 'categories' - type R = FindJoinTableRelationship - let expected: { - foreignKeyName: 'product_categories_category_id_fkey' - columns: ['category_id'] - isOneToOne: false - referencedRelation: 'categories' - referencedColumns: ['id'] - } - expectType(expected!) -} - -{ - type Schema = Database['public'] - type CurrentTableOrView = 'categories' - type FieldName = 'products' - type R = FindJoinTableRelationship - let expected: { - foreignKeyName: 'product_categories_product_id_fkey' - columns: ['product_id'] - isOneToOne: false - referencedRelation: 'products' - referencedColumns: ['id'] - } - expectType(expected!) -} - -{ - type Schema = Database['public'] - type CurrentTableOrView = 'categories' - type FieldName = 'missing' - type R = FindJoinTableRelationship - let expected: never - expectType(expected!) -} From 16380f964581b6ef69c0b9cd08c15fd949f96295 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 30 Oct 2024 11:16:03 +0100 Subject: [PATCH 04/31] chore: using type fix the test --- src/select-query-parser/result.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index cee0b150..56b0caf7 100644 --- a/src/select-query-parser/result.ts +++ b/src/select-query-parser/result.ts @@ -135,6 +135,7 @@ export type ProcessRPCNode< : NodeType['type'] extends Ast.FieldNode['type'] ? ProcessSimpleField> : SelectQueryError<'RPC Unsupported node type.'> + /** * Process select call that can be chained after an rpc call */ @@ -197,13 +198,13 @@ export type ProcessNode< RelationName extends string, Relationships extends GenericRelationship[], NodeType extends Ast.Node -> = NodeType extends Ast.StarNode // If the selection is * +> = NodeType['type'] extends Ast.StarNode['type'] // If the selection is * ? Row - : NodeType extends Ast.SpreadNode // If the selection is a ...spread - ? ProcessSpreadNode - : NodeType extends Ast.FieldNode - ? ProcessFieldNode - : SelectQueryError<'Unsupported node type.' & { nodeType: NodeType }> + : NodeType['type'] extends Ast.SpreadNode['type'] // If the selection is a ...spread + ? ProcessSpreadNode> + : NodeType['type'] extends Ast.FieldNode['type'] + ? ProcessFieldNode> + : SelectQueryError<'Unsupported node type.'> /** * Processes a FieldNode and returns the resulting TypeScript type. From f22849b2cc7a482da35a2582b2ffa3ecf513f700 Mon Sep 17 00:00:00 2001 From: Bobbie Soedirgo Date: Wed, 6 Nov 2024 02:00:52 +0800 Subject: [PATCH 05/31] chore: add TODO --- src/select-query-parser/result.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index 56b0caf7..88c0493e 100644 --- a/src/select-query-parser/result.ts +++ b/src/select-query-parser/result.ts @@ -198,13 +198,15 @@ export type ProcessNode< RelationName extends string, Relationships extends GenericRelationship[], NodeType extends Ast.Node -> = NodeType['type'] extends Ast.StarNode['type'] // If the selection is * - ? Row - : NodeType['type'] extends Ast.SpreadNode['type'] // If the selection is a ...spread - ? ProcessSpreadNode> - : NodeType['type'] extends Ast.FieldNode['type'] - ? ProcessFieldNode> - : SelectQueryError<'Unsupported node type.'> +> = + // TODO: figure out why comparing the `type` property is necessary vs. `NodeType extends Ast.StarNode` + NodeType['type'] extends Ast.StarNode['type'] // If the selection is * + ? Row + : NodeType['type'] extends Ast.SpreadNode['type'] // If the selection is a ...spread + ? ProcessSpreadNode> + : NodeType['type'] extends Ast.FieldNode['type'] + ? ProcessFieldNode> + : SelectQueryError<'Unsupported node type.'> /** * Processes a FieldNode and returns the resulting TypeScript type. From cc9344a8221d099fff0f0386de0cd0e819068751 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Wed, 6 Nov 2024 08:35:27 +0100 Subject: [PATCH 06/31] fix(types): make types retrocompatible to typescript 4.5.5 (#571) * fix(types): spread and embeding combination issues * chore: remove debug test case * chore: reproduce invalid node type error * chore: using type fix the test * fix(types): types result with schema as any * wip: make parser logic retrocompatible * wip: almost there * chore(types): make GetResult compatible with typescript 4.5.5 * chore: fix merge * chore: finish merge * chore: revert non essential changes * chore: add TODO --------- Co-authored-by: Bobbie Soedirgo Co-authored-by: Bobbie Soedirgo <31685197+soedirgo@users.noreply.github.com> --- package-lock.json | 15 +- package.json | 2 +- src/select-query-parser/parser.ts | 112 +++---- src/select-query-parser/result.ts | 70 ++-- src/select-query-parser/utils.ts | 375 ++++++++++++---------- test/relationships.ts | 26 ++ test/select-query-parser/result.test-d.ts | 4 +- test/select-query-parser/rpc.test-d.ts | 4 +- test/select-query-parser/select.test-d.ts | 2 +- 9 files changed, 337 insertions(+), 273 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5200b488..55395c0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "ts-jest": "^28.0.3", "tsd": "^0.31.2", "typedoc": "^0.22.16", - "typescript": "~4.7", + "typescript": "4.5.5", "wait-for-localhost-cli": "^3.0.0" } }, @@ -5978,10 +5978,11 @@ } }, "node_modules/typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", + "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10605,9 +10606,9 @@ } }, "typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", + "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", "dev": true }, "v8-to-istanbul": { diff --git a/package.json b/package.json index dda574d6..e440b5ab 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "ts-jest": "^28.0.3", "tsd": "^0.31.2", "typedoc": "^0.22.16", - "typescript": "~4.7", + "typescript": "4.5.5", "wait-for-localhost-cli": "^3.0.0" } } diff --git a/src/select-query-parser/parser.ts b/src/select-query-parser/parser.ts index 77a57e30..a3944d73 100644 --- a/src/select-query-parser/parser.ts +++ b/src/select-query-parser/parser.ts @@ -12,10 +12,12 @@ import { SimplifyDeep } from '../types' */ export type ParseQuery = string extends Query ? GenericStringError - : ParseNodes> extends [infer Nodes extends Ast.Node[], `${infer Remainder}`] - ? EatWhitespace extends '' - ? SimplifyDeep - : ParserError<`Unexpected input: ${Remainder}`> + : ParseNodes> extends [infer Nodes, `${infer Remainder}`] + ? Nodes extends Ast.Node[] + ? EatWhitespace extends '' + ? SimplifyDeep + : ParserError<`Unexpected input: ${Remainder}`> + : ParserError<'Invalid nodes array structure'> : ParseNodes> /** @@ -34,14 +36,15 @@ type ParseNodes = string extends Input : ParseNodesHelper type ParseNodesHelper = ParseNode extends [ - infer Node extends Ast.Node, + infer Node, `${infer Remainder}` ] - ? EatWhitespace extends `,${infer Remainder}` - ? ParseNodesHelper, [...Nodes, Node]> - : [[...Nodes, Node], EatWhitespace] + ? Node extends Ast.Node + ? EatWhitespace extends `,${infer Remainder}` + ? ParseNodesHelper, [...Nodes, Node]> + : [[...Nodes, Node], EatWhitespace] + : ParserError<'Invalid node type in nodes helper'> : ParseNode - /** * Parses a node. * A node is one of the following: @@ -57,11 +60,10 @@ type ParseNode = Input extends '' ? [Ast.StarNode, EatWhitespace] : // `...field` Input extends `...${infer Remainder}` - ? ParseField> extends [ - infer TargetField extends Ast.FieldNode, - `${infer Remainder}` - ] - ? [{ type: 'spread'; target: TargetField }, EatWhitespace] + ? ParseField> extends [infer TargetField, `${infer Remainder}`] + ? TargetField extends Ast.FieldNode + ? [{ type: 'spread'; target: TargetField }, EatWhitespace] + : ParserError<'Invalid target field type in spread'> : ParserError<`Unable to parse spread resource at \`${Input}\``> : ParseIdentifier extends [infer NameOrAlias, `${infer Remainder}`] ? EatWhitespace extends `::${infer _}` @@ -69,11 +71,10 @@ type ParseNode = Input extends '' ParseField : EatWhitespace extends `:${infer Remainder}` ? // `alias:` - ParseField> extends [ - infer Field extends Ast.FieldNode, - `${infer Remainder}` - ] - ? [Omit & { alias: NameOrAlias }, EatWhitespace] + ParseField> extends [infer Field, `${infer Remainder}`] + ? Field extends Ast.FieldNode + ? [Omit & { alias: NameOrAlias }, EatWhitespace] + : ParserError<'Invalid field type in alias parsing'> : ParserError<`Unable to parse renamed field at \`${Input}\``> : // Otherwise, just parse it as a field without alias. ParseField @@ -98,24 +99,22 @@ type ParseField = Input extends '' ? Name extends 'count' ? ParseCountField : Remainder extends `!inner${infer Remainder}` - ? ParseEmbeddedResource> extends [ - infer Children extends Ast.Node[], - `${infer Remainder}` - ] - ? // `field!inner(nodes)` - [{ type: 'field'; name: Name; innerJoin: true; children: Children }, Remainder] + ? ParseEmbeddedResource> extends [infer Children, `${infer Remainder}`] + ? Children extends Ast.Node[] + ? // `field!inner(nodes)` + [{ type: 'field'; name: Name; innerJoin: true; children: Children }, Remainder] + : ParserError<'Invalid children array in inner join'> : CreateParserErrorIfRequired< ParseEmbeddedResource>, `Expected embedded resource after "!inner" at \`${Remainder}\`` > : EatWhitespace extends `!left${infer Remainder}` - ? ParseEmbeddedResource> extends [ - infer Children extends Ast.Node[], - `${infer Remainder}` - ] - ? // `field!left(nodes)` - // !left is a noise word - treat it the same way as a non-`!inner`. - [{ type: 'field'; name: Name; children: Children }, EatWhitespace] + ? ParseEmbeddedResource> extends [infer Children, `${infer Remainder}`] + ? Children extends Ast.Node[] + ? // `field!left(nodes)` + // !left is a noise word - treat it the same way as a non-`!inner`. + [{ type: 'field'; name: Name; children: Children }, EatWhitespace] + : ParserError<'Invalid children array in left join'> : CreateParserErrorIfRequired< ParseEmbeddedResource>, `Expected embedded resource after "!left" at \`${EatWhitespace}\`` @@ -124,30 +123,36 @@ type ParseField = Input extends '' ? ParseIdentifier> extends [infer Hint, `${infer Remainder}`] ? EatWhitespace extends `!inner${infer Remainder}` ? ParseEmbeddedResource> extends [ - infer Children extends Ast.Node[], + infer Children, `${infer Remainder}` ] - ? // `field!hint!inner(nodes)` - [ - { type: 'field'; name: Name; hint: Hint; innerJoin: true; children: Children }, - EatWhitespace - ] + ? Children extends Ast.Node[] + ? // `field!hint!inner(nodes)` + [ + { type: 'field'; name: Name; hint: Hint; innerJoin: true; children: Children }, + EatWhitespace + ] + : ParserError<'Invalid children array in hint inner join'> : ParseEmbeddedResource> : ParseEmbeddedResource> extends [ - infer Children extends Ast.Node[], + infer Children, `${infer Remainder}` ] - ? // `field!hint(nodes)` - [{ type: 'field'; name: Name; hint: Hint; children: Children }, EatWhitespace] + ? Children extends Ast.Node[] + ? // `field!hint(nodes)` + [ + { type: 'field'; name: Name; hint: Hint; children: Children }, + EatWhitespace + ] + : ParserError<'Invalid children array in hint'> : ParseEmbeddedResource> : ParserError<`Expected identifier after "!" at \`${EatWhitespace}\``> : EatWhitespace extends `(${infer _}` - ? ParseEmbeddedResource> extends [ - infer Children extends Ast.Node[], - `${infer Remainder}` - ] - ? // `field(nodes)` - [{ type: 'field'; name: Name; children: Children }, EatWhitespace] + ? ParseEmbeddedResource> extends [infer Children, `${infer Remainder}`] + ? Children extends Ast.Node[] + ? // `field(nodes)` + [{ type: 'field'; name: Name; children: Children }, EatWhitespace] + : ParserError<'Invalid children array in field'> : // Return error if start of embedded resource was detected but not found. ParseEmbeddedResource> : // Otherwise it's a non-embedded resource field. @@ -184,13 +189,12 @@ type ParseCountField = ParseIdentifier extends [ type ParseEmbeddedResource = Input extends `(${infer Remainder}` ? EatWhitespace extends `)${infer Remainder}` ? [[], EatWhitespace] - : ParseNodes> extends [ - infer Nodes extends Ast.Node[], - `${infer Remainder}` - ] - ? EatWhitespace extends `)${infer Remainder}` - ? [Nodes, EatWhitespace] - : ParserError<`Expected ")" at \`${EatWhitespace}\``> + : ParseNodes> extends [infer Nodes, `${infer Remainder}`] + ? Nodes extends Ast.Node[] + ? EatWhitespace extends `)${infer Remainder}` + ? [Nodes, EatWhitespace] + : ParserError<`Expected ")" at \`${EatWhitespace}\``> + : ParserError<'Invalid nodes array in embedded resource'> : ParseNodes> : ParserError<`Expected "(" at \`${Input}\``> diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index 88c0493e..41928ced 100644 --- a/src/select-query-parser/result.ts +++ b/src/select-query-parser/result.ts @@ -35,14 +35,18 @@ export type GetResult< Relationships, Query extends string > = IsAny extends true - ? ParseQuery extends infer ParsedQuery extends Ast.Node[] - ? RelationName extends string - ? ProcessNodesWithoutSchema - : any + ? ParseQuery extends infer ParsedQuery + ? ParsedQuery extends Ast.Node[] + ? RelationName extends string + ? ProcessNodesWithoutSchema + : any + : ParsedQuery : any : Relationships extends null // For .rpc calls the passed relationships will be null in that case, the result will always be the function return type - ? ParseQuery extends infer ParsedQuery extends Ast.Node[] - ? RPCCallNodes + ? ParseQuery extends infer ParsedQuery + ? ParsedQuery extends Ast.Node[] + ? RPCCallNodes + : ParsedQuery : Row : ParseQuery extends infer ParsedQuery ? ParsedQuery extends Ast.Node[] @@ -111,11 +115,15 @@ type ProcessNodeWithoutSchema = Node extends Ast.StarNode type ProcessNodesWithoutSchema< Nodes extends Ast.Node[], Acc extends Record = {} -> = Nodes extends [infer FirstNode extends Ast.Node, ...infer RestNodes extends Ast.Node[]] - ? ProcessNodeWithoutSchema extends infer FieldResult - ? FieldResult extends Record - ? ProcessNodesWithoutSchema - : FieldResult +> = Nodes extends [infer FirstNode, ...infer RestNodes] + ? FirstNode extends Ast.Node + ? RestNodes extends Ast.Node[] + ? ProcessNodeWithoutSchema extends infer FieldResult + ? FieldResult extends Record + ? ProcessNodesWithoutSchema + : FieldResult + : any + : any : any : Prettify @@ -144,14 +152,18 @@ export type RPCCallNodes< RelationName extends string, Row extends Record, Acc extends Record = {} // Acc is now an object -> = Nodes extends [infer FirstNode extends Ast.Node, ...infer RestNodes extends Ast.Node[]] - ? ProcessRPCNode extends infer FieldResult - ? FieldResult extends Record - ? RPCCallNodes - : FieldResult extends SelectQueryError - ? SelectQueryError - : SelectQueryError<'Could not retrieve a valid record or error value'> - : SelectQueryError<'Processing node failed.'> +> = Nodes extends [infer FirstNode, ...infer RestNodes] + ? FirstNode extends Ast.Node + ? RestNodes extends Ast.Node[] + ? ProcessRPCNode extends infer FieldResult + ? FieldResult extends Record + ? RPCCallNodes + : FieldResult extends SelectQueryError + ? SelectQueryError + : SelectQueryError<'Could not retrieve a valid record or error value'> + : SelectQueryError<'Processing node failed.'> + : SelectQueryError<'Invalid rest nodes array in RPC call'> + : SelectQueryError<'Invalid first node in RPC call'> : Prettify /** @@ -172,14 +184,18 @@ export type ProcessNodes< Nodes extends Ast.Node[], Acc extends Record = {} // Acc is now an object > = CheckDuplicateEmbededReference extends false - ? Nodes extends [infer FirstNode extends Ast.Node, ...infer RestNodes extends Ast.Node[]] - ? ProcessNode extends infer FieldResult - ? FieldResult extends Record - ? ProcessNodes - : FieldResult extends SelectQueryError - ? SelectQueryError - : SelectQueryError<'Could not retrieve a valid record or error value'> - : SelectQueryError<'Processing node failed.'> + ? Nodes extends [infer FirstNode, ...infer RestNodes] + ? FirstNode extends Ast.Node + ? RestNodes extends Ast.Node[] + ? ProcessNode extends infer FieldResult + ? FieldResult extends Record + ? ProcessNodes + : FieldResult extends SelectQueryError + ? SelectQueryError + : SelectQueryError<'Could not retrieve a valid record or error value'> + : SelectQueryError<'Processing node failed.'> + : SelectQueryError<'Invalid rest nodes array type in ProcessNodes'> + : SelectQueryError<'Invalid first node type in ProcessNodes'> : Prettify : Prettify> diff --git a/src/select-query-parser/utils.ts b/src/select-query-parser/utils.ts index 90ca4d60..1b1a3bea 100644 --- a/src/select-query-parser/utils.ts +++ b/src/select-query-parser/utils.ts @@ -38,27 +38,24 @@ type ResolveRelationships< Relationships extends GenericRelationship[], Nodes extends Ast.FieldNode[] > = UnionToArray<{ - [K in keyof Nodes]: ResolveRelationship< - Schema, - Relationships, - Nodes[K], - RelationName - > extends infer Relation - ? Relation extends { - relation: { - referencedRelation: any - foreignKeyName: any - match: any - } - from: any - } - ? { - referencedTable: Relation['relation']['referencedRelation'] - fkName: Relation['relation']['foreignKeyName'] - from: Relation['from'] - match: Relation['relation']['match'] - fieldName: GetFieldNodeResultName + [K in keyof Nodes]: Nodes[K] extends Ast.FieldNode + ? ResolveRelationship extends infer Relation + ? Relation extends { + relation: { + referencedRelation: string + foreignKeyName: string + match: string + } + from: string } + ? { + referencedTable: Relation['relation']['referencedRelation'] + fkName: Relation['relation']['foreignKeyName'] + from: Relation['from'] + match: Relation['relation']['match'] + fieldName: GetFieldNodeResultName + } + : Relation : never : never }>[0] @@ -69,10 +66,12 @@ type ResolveRelationships< type IsDoubleReference = T extends { referencedTable: infer RT fieldName: infer FN - match: infer M extends 'col' | 'refrel' + match: infer M } - ? U extends { referencedTable: RT; fieldName: FN; match: M } - ? true + ? M extends 'col' | 'refrel' + ? U extends { referencedTable: RT; fieldName: FN; match: M } + ? true + : false : false : false @@ -97,21 +96,25 @@ export type CheckDuplicateEmbededReference< RelationName extends string, Relationships extends GenericRelationship[], Nodes extends Ast.Node[] -> = FilterRelationNodes extends infer RelationsNodes extends Ast.FieldNode[] - ? ResolveRelationships< - Schema, - RelationName, - Relationships, - RelationsNodes - > extends infer ResolvedRels - ? ResolvedRels extends unknown[] - ? FindDuplicates extends infer Duplicates - ? Duplicates extends never - ? false - : Duplicates extends { fieldName: infer FieldName extends string } - ? { - [K in FieldName]: SelectQueryError<`table "${RelationName}" specified more than once use hinting for desambiguation`> - } +> = FilterRelationNodes extends infer RelationsNodes + ? RelationsNodes extends Ast.FieldNode[] + ? ResolveRelationships< + Schema, + RelationName, + Relationships, + RelationsNodes + > extends infer ResolvedRels + ? ResolvedRels extends unknown[] + ? FindDuplicates extends infer Duplicates + ? Duplicates extends never + ? false + : Duplicates extends { fieldName: infer FieldName } + ? FieldName extends string + ? { + [K in FieldName]: SelectQueryError<`table "${RelationName}" specified more than once use hinting for desambiguation`> + } + : false + : false : false : false : false @@ -155,33 +158,38 @@ type CheckRelationshipError< : // If the relation is a reverse relation with no hint (matching by name) FoundRelation extends { relation: { - referencedRelation: infer RelatedRelationName extends string + referencedRelation: infer RelatedRelationName name: string } direction: 'reverse' } - ? // We check if there is possible confusion with other relations with this table - HasMultipleFKeysToFRel extends true - ? // If there is, postgrest will fail at runtime, and require desambiguation via hinting - SelectQueryError<`Could not embed because more than one relationship was found for '${RelatedRelationName}' and '${CurrentTableOrView}' you need to hint the column with ${RelatedRelationName}! ?`> - : FoundRelation + ? RelatedRelationName extends string + ? // We check if there is possible confusion with other relations with this table + HasMultipleFKeysToFRel extends true + ? // If there is, postgrest will fail at runtime, and require desambiguation via hinting + SelectQueryError<`Could not embed because more than one relationship was found for '${RelatedRelationName}' and '${CurrentTableOrView}' you need to hint the column with ${RelatedRelationName}! ?`> + : FoundRelation + : never : // Same check for forward relationships, but we must gather the relationships from the found relation FoundRelation extends { relation: { - referencedRelation: infer RelatedRelationName extends string + referencedRelation: infer RelatedRelationName name: string } direction: 'forward' - from: infer From extends keyof TablesAndViews & string + from: infer From } - ? HasMultipleFKeysToFRel< - RelatedRelationName, - TablesAndViews[From]['Relationships'] - > extends true - ? SelectQueryError<`Could not embed because more than one relationship was found for '${From}' and '${RelatedRelationName}' you need to hint the column with ${From}! ?`> - : FoundRelation + ? RelatedRelationName extends string + ? From extends keyof TablesAndViews & string + ? HasMultipleFKeysToFRel< + RelatedRelationName, + TablesAndViews[From]['Relationships'] + > extends true + ? SelectQueryError<`Could not embed because more than one relationship was found for '${From}' and '${RelatedRelationName}' you need to hint the column with ${From}! ?`> + : FoundRelation + : never + : never : FoundRelation - /** * Resolves relationships for embedded resources and retrieves the referenced Table */ @@ -217,26 +225,28 @@ type ResolveReverseRelationship< > = FindFieldMatchingRelationships extends infer FoundRelation ? FoundRelation extends never ? false - : FoundRelation extends { referencedRelation: infer RelatedRelationName extends string } - ? RelatedRelationName extends keyof TablesAndViews - ? // If the relation was found via hinting we just return it without any more checks - FoundRelation extends { hint: string } - ? { - referencedTable: TablesAndViews[RelatedRelationName] - relation: FoundRelation - direction: 'reverse' - from: CurrentTableOrView - } - : // If the relation was found via implicit relation naming, we must ensure there is no conflicting matches - HasMultipleFKeysToFRel extends true - ? SelectQueryError<`Could not embed because more than one relationship was found for '${RelatedRelationName}' and '${CurrentTableOrView}' you need to hint the column with ${RelatedRelationName}! ?`> - : { - referencedTable: TablesAndViews[RelatedRelationName] - relation: FoundRelation - direction: 'reverse' - from: CurrentTableOrView - } - : SelectQueryError<`Relation '${RelatedRelationName}' not found in schema.`> + : FoundRelation extends { referencedRelation: infer RelatedRelationName } + ? RelatedRelationName extends string + ? RelatedRelationName extends keyof TablesAndViews + ? // If the relation was found via hinting we just return it without any more checks + FoundRelation extends { hint: string } + ? { + referencedTable: TablesAndViews[RelatedRelationName] + relation: FoundRelation + direction: 'reverse' + from: CurrentTableOrView + } + : // If the relation was found via implicit relation naming, we must ensure there is no conflicting matches + HasMultipleFKeysToFRel extends true + ? SelectQueryError<`Could not embed because more than one relationship was found for '${RelatedRelationName}' and '${CurrentTableOrView}' you need to hint the column with ${RelatedRelationName}! ?`> + : { + referencedTable: TablesAndViews[RelatedRelationName] + relation: FoundRelation + direction: 'reverse' + from: CurrentTableOrView + } + : SelectQueryError<`Relation '${RelatedRelationName}' not found in schema.`> + : false : false : false @@ -244,17 +254,19 @@ export type FindMatchingTableRelationships< Schema extends GenericSchema, Relationships extends GenericRelationship[], value extends string -> = Relationships extends [infer R, ...infer Rest extends GenericRelationship[]] - ? R extends { referencedRelation: infer ReferencedRelation } - ? ReferencedRelation extends keyof Schema['Tables'] - ? R extends { foreignKeyName: value } - ? R & { match: 'fkname' } - : R extends { referencedRelation: value } - ? R & { match: 'refrel' } - : R extends { columns: [value] } - ? R & { match: 'col' } +> = Relationships extends [infer R, ...infer Rest] + ? Rest extends GenericRelationship[] + ? R extends { referencedRelation: infer ReferencedRelation } + ? ReferencedRelation extends keyof Schema['Tables'] + ? R extends { foreignKeyName: value } + ? R & { match: 'fkname' } + : R extends { referencedRelation: value } + ? R & { match: 'refrel' } + : R extends { columns: [value] } + ? R & { match: 'col' } + : FindMatchingTableRelationships : FindMatchingTableRelationships - : FindMatchingTableRelationships + : false : false : false @@ -262,17 +274,19 @@ export type FindMatchingViewRelationships< Schema extends GenericSchema, Relationships extends GenericRelationship[], value extends string -> = Relationships extends [infer R, ...infer Rest extends GenericRelationship[]] - ? R extends { referencedRelation: infer ReferencedRelation } - ? ReferencedRelation extends keyof Schema['Views'] - ? R extends { foreignKeyName: value } - ? R & { match: 'fkname' } - : R extends { referencedRelation: value } - ? R & { match: 'refrel' } - : R extends { columns: [value] } - ? R & { match: 'col' } +> = Relationships extends [infer R, ...infer Rest] + ? Rest extends GenericRelationship[] + ? R extends { referencedRelation: infer ReferencedRelation } + ? ReferencedRelation extends keyof Schema['Views'] + ? R extends { foreignKeyName: value } + ? R & { match: 'fkname' } + : R extends { referencedRelation: value } + ? R & { match: 'refrel' } + : R extends { columns: [value] } + ? R & { match: 'col' } + : FindMatchingViewRelationships : FindMatchingViewRelationships - : FindMatchingViewRelationships + : false : false : false @@ -281,36 +295,39 @@ export type FindMatchingHintTableRelationships< Relationships extends GenericRelationship[], hint extends string, name extends string -> = Relationships extends [infer R, ...infer Rest extends GenericRelationship[]] - ? R extends { referencedRelation: infer ReferencedRelation } - ? ReferencedRelation extends name - ? R extends { foreignKeyName: hint } - ? R & { match: 'fkname' } - : R extends { referencedRelation: hint } - ? R & { match: 'refrel' } - : R extends { columns: [hint] } - ? R & { match: 'col' } +> = Relationships extends [infer R, ...infer Rest] + ? Rest extends GenericRelationship[] + ? R extends { referencedRelation: infer ReferencedRelation } + ? ReferencedRelation extends name + ? R extends { foreignKeyName: hint } + ? R & { match: 'fkname' } + : R extends { referencedRelation: hint } + ? R & { match: 'refrel' } + : R extends { columns: [hint] } + ? R & { match: 'col' } + : FindMatchingHintTableRelationships : FindMatchingHintTableRelationships - : FindMatchingHintTableRelationships + : false : false : false - export type FindMatchingHintViewRelationships< Schema extends GenericSchema, Relationships extends GenericRelationship[], hint extends string, name extends string -> = Relationships extends [infer R, ...infer Rest extends GenericRelationship[]] - ? R extends { referencedRelation: infer ReferencedRelation } - ? ReferencedRelation extends name - ? R extends { foreignKeyName: hint } - ? R & { match: 'fkname' } - : R extends { referencedRelation: hint } - ? R & { match: 'refrel' } - : R extends { columns: [hint] } - ? R & { match: 'col' } +> = Relationships extends [infer R, ...infer Rest] + ? Rest extends GenericRelationship[] + ? R extends { referencedRelation: infer ReferencedRelation } + ? ReferencedRelation extends name + ? R extends { foreignKeyName: hint } + ? R & { match: 'fkname' } + : R extends { referencedRelation: hint } + ? R & { match: 'refrel' } + : R extends { columns: [hint] } + ? R & { match: 'col' } + : FindMatchingHintViewRelationships : FindMatchingHintViewRelationships - : FindMatchingHintViewRelationships + : false : false : false @@ -337,8 +354,10 @@ type TableForwardRelationships< > = TName extends keyof TablesAndViews ? UnionToArray< RecursivelyFindRelationships> - > extends infer R extends (GenericRelationship & { from: keyof TablesAndViews })[] - ? R + > extends infer R + ? R extends (GenericRelationship & { from: keyof TablesAndViews })[] + ? R + : [] : [] : [] @@ -362,8 +381,7 @@ type FilterRelationships = R extends readonly (infer Rel)[] : never : never -// Find a relationship from the parent to the childrens -type ResolveForwardRelationship< +export type ResolveForwardRelationship< Schema extends GenericSchema, Field extends Ast.FieldNode, CurrentTableOrView extends keyof TablesAndViews & string @@ -371,46 +389,46 @@ type ResolveForwardRelationship< Schema, TablesAndViews[Field['name']]['Relationships'], Ast.FieldNode & { name: CurrentTableOrView; hint: Field['hint'] } -> extends infer FoundByName extends GenericRelationship - ? { - referencedTable: TablesAndViews[Field['name']] - relation: FoundByName - direction: 'forward' - from: Field['name'] - type: 'found-by-name' - } - : // The Field['name'] can sometimes be a reference to the related foreign key - // In that case, we can't use the Field['name'] to get back the relations, instead, we will find all relations pointing - // to our current table or view, and search if we can find a match in it - FindFieldMatchingRelationships< - Schema, - TableForwardRelationships, - Field - > extends infer FoundByMatch extends GenericRelationship & { - from: keyof TablesAndViews - } - ? { - referencedTable: TablesAndViews[FoundByMatch['from']] - relation: FoundByMatch - direction: 'forward' - from: CurrentTableOrView - type: 'found-by-match' - } - : // Forward relations can also alias other tables via tables joins relationships - // in such cases we crawl all the tables looking for a join table between our current table - // and the Field['name'] desired desitnation - FindJoinTableRelationship< - Schema, - CurrentTableOrView, - Field['name'] - > extends infer FoundByJoinTable extends GenericRelationship - ? { - referencedTable: TablesAndViews[FoundByJoinTable['referencedRelation']] - relation: FoundByJoinTable & { match: 'refrel' } - direction: 'forward' - from: CurrentTableOrView - type: 'found-by-join-table' - } +> extends infer FoundByName + ? FoundByName extends GenericRelationship + ? { + referencedTable: TablesAndViews[Field['name']] + relation: FoundByName + direction: 'forward' + from: Field['name'] + type: 'found-by-name' + } + : FindFieldMatchingRelationships< + Schema, + TableForwardRelationships, + Field + > extends infer FoundByMatch + ? FoundByMatch extends GenericRelationship & { + from: keyof TablesAndViews + } + ? { + referencedTable: TablesAndViews[FoundByMatch['from']] + relation: FoundByMatch + direction: 'forward' + from: CurrentTableOrView + type: 'found-by-match' + } + : FindJoinTableRelationship< + Schema, + CurrentTableOrView, + Field['name'] + > extends infer FoundByJoinTable + ? FoundByJoinTable extends GenericRelationship + ? { + referencedTable: TablesAndViews[FoundByJoinTable['referencedRelation']] + relation: FoundByJoinTable & { match: 'refrel' } + direction: 'forward' + from: CurrentTableOrView + type: 'found-by-join-table' + } + : SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`> + : SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`> + : SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`> : SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`> /** @@ -431,7 +449,7 @@ type ResolveForwardRelationship< * referencedColumns: ["id"] * } */ -export type FindJoinTableRelationship< +type ResolveJoinTableRelationship< Schema extends GenericSchema, CurrentTableOrView extends keyof TablesAndViews & string, FieldName extends string @@ -447,6 +465,15 @@ export type FindJoinTableRelationship< : never }[keyof TablesAndViews] +export type FindJoinTableRelationship< + Schema extends GenericSchema, + CurrentTableOrView extends keyof TablesAndViews & string, + FieldName extends string +> = ResolveJoinTableRelationship extends infer Result + ? [Result] extends [never] + ? false + : Result + : never /** * Finds a matching relationship based on the FieldNode's name and optional hint. */ @@ -454,43 +481,35 @@ export type FindFieldMatchingRelationships< Schema extends GenericSchema, Relationships extends GenericRelationship[], Field extends Ast.FieldNode -> = Field extends { hint: infer Hint extends string } +> = Field extends { hint: string } ? FindMatchingHintTableRelationships< Schema, Relationships, - Hint, + Field['hint'], Field['name'] - > extends infer TableRelationViaHint extends GenericRelationship - ? TableRelationViaHint & { + > extends GenericRelationship + ? FindMatchingHintTableRelationships & { branch: 'found-in-table-via-hint' hint: Field['hint'] } : FindMatchingHintViewRelationships< Schema, Relationships, - Hint, + Field['hint'], Field['name'] - > extends infer TableViewViaHint extends GenericRelationship - ? TableViewViaHint & { + > extends GenericRelationship + ? FindMatchingHintViewRelationships & { branch: 'found-in-view-via-hint' hint: Field['hint'] } : SelectQueryError<'Failed to find matching relation via hint'> - : FindMatchingTableRelationships< - Schema, - Relationships, - Field['name'] - > extends infer TableRelationViaName extends GenericRelationship - ? TableRelationViaName & { + : FindMatchingTableRelationships extends GenericRelationship + ? FindMatchingTableRelationships & { branch: 'found-in-table-via-name' name: Field['name'] } - : FindMatchingViewRelationships< - Schema, - Relationships, - Field['name'] - > extends infer ViewRelationViaName extends GenericRelationship - ? ViewRelationViaName & { + : FindMatchingViewRelationships extends GenericRelationship + ? FindMatchingViewRelationships & { branch: 'found-in-view-via-name' name: Field['name'] } diff --git a/test/relationships.ts b/test/relationships.ts index 6b3cf7a4..faca706a 100644 --- a/test/relationships.ts +++ b/test/relationships.ts @@ -185,6 +185,11 @@ export const selectParams = { from: 'products', select: '*, categories(*)', }, + nestedQueryWithSelectiveFieldsAndInnerJoin: { + from: 'users', + select: + 'msgs:messages(id, ...message_details(created_at, channel!inner(id, slug, owner:users(*))))', + }, } as const export const selectQueries = { @@ -363,6 +368,9 @@ export const selectQueries = { manyToManyWithJoinTable: postgrest .from(selectParams.manyToManyWithJoinTable.from) .select(selectParams.manyToManyWithJoinTable.select), + nestedQueryWithSelectiveFieldsAndInnerJoin: postgrest + .from(selectParams.nestedQueryWithSelectiveFieldsAndInnerJoin.from) + .select(selectParams.nestedQueryWithSelectiveFieldsAndInnerJoin.select), } as const test('nested query with selective fields', async () => { @@ -1868,3 +1876,21 @@ test('many-to-many with join table', async () => { } `) }) + +test('nested query with selective fields and inner join should error on non existing relation', async () => { + const res = await selectQueries.nestedQueryWithSelectiveFieldsAndInnerJoin.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST200", + "details": "Searched for a foreign key relationship between 'messages' and 'message_details' in the schema 'public', but no matches were found.", + "hint": null, + "message": "Could not find a relationship between 'messages' and 'message_details' in the schema cache", + }, + "status": 400, + "statusText": "Bad Request", + } + `) +}) diff --git a/test/select-query-parser/result.test-d.ts b/test/select-query-parser/result.test-d.ts index ff841096..74a40836 100644 --- a/test/select-query-parser/result.test-d.ts +++ b/test/select-query-parser/result.test-d.ts @@ -72,7 +72,7 @@ type SelectQueryFromTableResult< expectType>(true) } -// nested query with selective fields +// nested query with selective fields and inner join should error on non existing relation { let result: SelectQueryFromTableResult< 'users', @@ -81,7 +81,7 @@ type SelectQueryFromTableResult< let expected: { msgs: { id: number - message_details: SelectQueryError<`Could not embed because more than one relationship was found for 'messages' and '${string}' you need to hint the column with messages! ?`> + message_details: SelectQueryError<'could not find the relation between messages and message_details'> }[] } expectType>(true) diff --git a/test/select-query-parser/rpc.test-d.ts b/test/select-query-parser/rpc.test-d.ts index 858434bf..f22b915b 100644 --- a/test/select-query-parser/rpc.test-d.ts +++ b/test/select-query-parser/rpc.test-d.ts @@ -5,9 +5,7 @@ import { TypeEqual } from 'ts-expect' // RPC call with no params { - const { data } = await postgrest - .rpc(RPC_NAME, { name_param: 'supabot' }) - .select(selectParams.noParams) + const { data } = await postgrest.rpc(RPC_NAME, { name_param: 'supabot' }).select() let result: Exclude let expected: Database['public']['Functions'][typeof RPC_NAME]['Returns'] expectType>(true) diff --git a/test/select-query-parser/select.test-d.ts b/test/select-query-parser/select.test-d.ts index af2fd540..2343fa6f 100644 --- a/test/select-query-parser/select.test-d.ts +++ b/test/select-query-parser/select.test-d.ts @@ -741,7 +741,7 @@ type Schema = Database['public'] { const { data, error } = await selectQueries.aggregateOnMissingColumnWithAlias.limit(1).single() if (error) throw error - expectType>(data) + expectType>(data!) } // many-to-many with join table From fe72d4599de8b0df9b6c97fb9ce9072ad7c3dcb5 Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 7 Nov 2024 11:06:15 +0100 Subject: [PATCH 07/31] fix(types): add deduplication helpers for retro-compatibility --- src/select-query-parser/utils.ts | 44 ++++++++++++--- test/select-query-parser/types.test-d.ts | 55 +++++++++++++++++++ test/types.ts | 70 ++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 test/select-query-parser/types.test-d.ts diff --git a/src/select-query-parser/utils.ts b/src/select-query-parser/utils.ts index 1b1a3bea..b3691e93 100644 --- a/src/select-query-parser/utils.ts +++ b/src/select-query-parser/utils.ts @@ -14,6 +14,23 @@ export type IsAny = 0 extends 1 & T ? true : false export type SelectQueryError = { error: true } & Message +/* + ** Because of pg-meta types generation there is some cases where a same relationship can be duplicated + ** if the relation is across schemas and views this ensure that we dedup those relations and treat them + ** as postgrest would. + ** This is no longer the case and has been patched here: https://github.com/supabase/postgres-meta/pull/809 + ** But we still need this for retro-compatibilty with older generated types + ** TODO: Remove this in next major version + */ +export type DeduplicateRelationships = T extends readonly [ + infer First, + ...infer Rest +] + ? First extends Rest[number] + ? DeduplicateRelationships + : [First, ...DeduplicateRelationships] + : T + export type GetFieldNodeResultName = Field['alias'] extends string ? Field['alias'] : Field['aggregateFunction'] extends AggregateFunctions @@ -87,10 +104,14 @@ type CheckDuplicates = Arr extends [infer Head, ...i /** * Iterates over the elements of the array to find duplicates */ -type FindDuplicates = Arr extends [infer Head, ...infer Tail] - ? CheckDuplicates | FindDuplicates +type FindDuplicatesWithinDeduplicated = Arr extends [infer Head, ...infer Tail] + ? CheckDuplicates | FindDuplicatesWithinDeduplicated : never +type FindDuplicates = FindDuplicatesWithinDeduplicated< + DeduplicateRelationships +> + export type CheckDuplicateEmbededReference< Schema extends GenericSchema, RelationName extends string, @@ -137,17 +158,22 @@ type HasFKeyToFRel = Relationships extends [infer R] /** * Checks if there is more than one relation to a given foreign relation name in the Relationships. */ -type HasMultipleFKeysToFRel = Relationships extends [ +type HasMultipleFKeysToFRelDeduplicated = Relationships extends [ infer R, ...infer Rest ] ? R extends { referencedRelation: FRelName } ? HasFKeyToFRel extends true ? true - : HasMultipleFKeysToFRel - : HasMultipleFKeysToFRel + : HasMultipleFKeysToFRelDeduplicated + : HasMultipleFKeysToFRelDeduplicated : false +type HasMultipleFKeysToFRel< + FRelName, + Relationships extends unknown[] +> = HasMultipleFKeysToFRelDeduplicated> + type CheckRelationshipError< Schema extends GenericSchema, Relationships extends GenericRelationship[], @@ -454,9 +480,13 @@ type ResolveJoinTableRelationship< CurrentTableOrView extends keyof TablesAndViews & string, FieldName extends string > = { - [TableName in keyof TablesAndViews]: TablesAndViews[TableName]['Relationships'] extends readonly (infer Rel)[] + [TableName in keyof TablesAndViews]: DeduplicateRelationships< + TablesAndViews[TableName]['Relationships'] + > extends readonly (infer Rel)[] ? Rel extends { referencedRelation: CurrentTableOrView } - ? TablesAndViews[TableName]['Relationships'] extends readonly (infer OtherRel)[] + ? DeduplicateRelationships< + TablesAndViews[TableName]['Relationships'] + > extends readonly (infer OtherRel)[] ? OtherRel extends { referencedRelation: FieldName } ? OtherRel : never diff --git a/test/select-query-parser/types.test-d.ts b/test/select-query-parser/types.test-d.ts new file mode 100644 index 00000000..d52432df --- /dev/null +++ b/test/select-query-parser/types.test-d.ts @@ -0,0 +1,55 @@ +import { expectType } from 'tsd' +import { TypeEqual } from 'ts-expect' +import { DeduplicateRelationships } from '../../src/select-query-parser/utils' +// Deduplicate exact sames relationships +{ + type rels = [ + { + foreignKeyName: 'test_fkey' + columns: ['project_id'] + referencedRelation: 'project_subscriptions' + referencedColumns: ['project_id'] + }, + { + foreignKeyName: 'test_fkey' + columns: ['project_id'] + referencedRelation: 'projects' + referencedColumns: ['id'] + }, + { + foreignKeyName: 'test_fkey' + columns: ['project_id'] + referencedRelation: 'projects' + referencedColumns: ['id'] + }, + { + foreignKeyName: 'test_fkey' + columns: ['project_id'] + referencedRelation: 'sls_physical_backups_monitoring' + referencedColumns: ['project_id'] + } + ] + type expected = [ + { + foreignKeyName: 'test_fkey' + columns: ['project_id'] + referencedRelation: 'project_subscriptions' + referencedColumns: ['project_id'] + }, + { + foreignKeyName: 'test_fkey' + columns: ['project_id'] + referencedRelation: 'projects' + referencedColumns: ['id'] + }, + { + foreignKeyName: 'test_fkey' + columns: ['project_id'] + referencedRelation: 'sls_physical_backups_monitoring' + referencedColumns: ['project_id'] + } + ] + + type result = DeduplicateRelationships + expectType>(true) +} diff --git a/test/types.ts b/test/types.ts index bd26dd1e..2ed1d1c4 100644 --- a/test/types.ts +++ b/test/types.ts @@ -86,6 +86,13 @@ export type Database = { referencedRelation: 'users' referencedColumns: ['username'] }, + { + foreignKeyName: 'best_friends_first_user_fkey' + columns: ['first_user'] + isOneToOne: false + referencedRelation: 'users' + referencedColumns: ['username'] + }, { foreignKeyName: 'best_friends_second_user_fkey' columns: ['second_user'] @@ -107,6 +114,13 @@ export type Database = { referencedRelation: 'users' referencedColumns: ['username'] }, + { + foreignKeyName: 'best_friends_second_user_fkey' + columns: ['second_user'] + isOneToOne: false + referencedRelation: 'users' + referencedColumns: ['username'] + }, { foreignKeyName: 'best_friends_third_wheel_fkey' columns: ['third_wheel'] @@ -121,6 +135,13 @@ export type Database = { referencedRelation: 'updatable_view' referencedColumns: ['username'] }, + { + foreignKeyName: 'best_friends_third_wheel_fkey' + columns: ['third_wheel'] + isOneToOne: false + referencedRelation: 'users' + referencedColumns: ['username'] + }, { foreignKeyName: 'best_friends_third_wheel_fkey' columns: ['third_wheel'] @@ -144,6 +165,13 @@ export type Database = { id?: number } Relationships: [ + { + foreignKeyName: 'channel_details_id_fkey' + columns: ['id'] + isOneToOne: true + referencedRelation: 'channels' + referencedColumns: ['id'] + }, { foreignKeyName: 'channel_details_id_fkey' columns: ['id'] @@ -188,6 +216,13 @@ export type Database = { parent_id?: number | null } Relationships: [ + { + foreignKeyName: 'collections_parent_id_fkey' + columns: ['parent_id'] + isOneToOne: false + referencedRelation: 'collections' + referencedColumns: ['id'] + }, { foreignKeyName: 'collections_parent_id_fkey' columns: ['parent_id'] @@ -220,6 +255,13 @@ export type Database = { username?: string } Relationships: [ + { + foreignKeyName: 'messages_channel_id_fkey' + columns: ['channel_id'] + isOneToOne: false + referencedRelation: 'channels' + referencedColumns: ['id'] + }, { foreignKeyName: 'messages_channel_id_fkey' columns: ['channel_id'] @@ -241,6 +283,13 @@ export type Database = { referencedRelation: 'updatable_view' referencedColumns: ['username'] }, + { + foreignKeyName: 'messages_username_fkey' + columns: ['username'] + isOneToOne: false + referencedRelation: 'users' + referencedColumns: ['username'] + }, { foreignKeyName: 'messages_username_fkey' columns: ['username'] @@ -264,6 +313,20 @@ export type Database = { product_id?: number } Relationships: [ + { + foreignKeyName: 'product_categories_category_id_fkey' + columns: ['category_id'] + isOneToOne: false + referencedRelation: 'categories' + referencedColumns: ['id'] + }, + { + foreignKeyName: 'product_categories_product_id_fkey' + columns: ['product_id'] + isOneToOne: false + referencedRelation: 'products' + referencedColumns: ['id'] + }, { foreignKeyName: 'product_categories_category_id_fkey' columns: ['category_id'] @@ -389,6 +452,13 @@ export type Database = { referencedRelation: 'updatable_view' referencedColumns: ['username'] }, + { + foreignKeyName: 'user_profiles_username_fkey' + columns: ['username'] + isOneToOne: false + referencedRelation: 'users' + referencedColumns: ['username'] + }, { foreignKeyName: 'user_profiles_username_fkey' columns: ['username'] From 74b99bbee1030215d0ebbbfc6deadeaea032352e Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 13 Nov 2024 10:49:43 +0100 Subject: [PATCH 08/31] fix(types): inference for default any schema --- src/select-query-parser/result.ts | 12 +++--------- test/select-query-parser/default-inference-d.ts | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index 41928ced..4af7d05e 100644 --- a/src/select-query-parser/result.ts +++ b/src/select-query-parser/result.ts @@ -78,15 +78,9 @@ type ProcessFieldNodeWithoutSchema = IsNonEmptyArray Node['children'] > extends true ? { - [K in Node['name']]: Node['children'] extends Ast.StarNode[] - ? any[] - : Node['children'] extends Ast.FieldNode[] - ? { - [P in Node['children'][number] as GetFieldNodeResultName

]: P['castType'] extends PostgreSQLTypes - ? TypeScriptTypes - : any - }[] - : any[] + [K in GetFieldNodeResultName]: Node['children'] extends Ast.Node[] + ? ProcessNodesWithoutSchema[] + : ProcessSimpleFieldWithoutSchema } : ProcessSimpleFieldWithoutSchema diff --git a/test/select-query-parser/default-inference-d.ts b/test/select-query-parser/default-inference-d.ts index 98b2ba95..3ca463b5 100644 --- a/test/select-query-parser/default-inference-d.ts +++ b/test/select-query-parser/default-inference-d.ts @@ -35,6 +35,22 @@ const REST_URL = 'http://localhost:3000' } expectType>(true) } +// embeding renaming +{ + const postgrest = new PostgrestClient(REST_URL) + const { data } = await postgrest + .from('projects') + .select('status,service:services(service_api_keys(*))') + .single() + let result: Exclude + let expected: { + status: any + service: { + service_api_keys: any[] + }[] + } + expectType>(true) +} // spread operator with stars should return any { const postgrest = new PostgrestClient(REST_URL) From 0af65f55da930dc0b13d18a5c6994a72540c79f0 Mon Sep 17 00:00:00 2001 From: Krzysztof Borowy <6444719+krizzu@users.noreply.github.com> Date: Sun, 17 Nov 2024 22:28:38 +0100 Subject: [PATCH 09/31] fix: Add missing PostgrestError export to wrapper.mjs As per title --- wrapper.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wrapper.mjs b/wrapper.mjs index 55629686..67b173b5 100644 --- a/wrapper.mjs +++ b/wrapper.mjs @@ -5,6 +5,7 @@ const { PostgrestFilterBuilder, PostgrestTransformBuilder, PostgrestBuilder, + PostgrestError, } = index export { @@ -13,6 +14,7 @@ export { PostgrestFilterBuilder, PostgrestQueryBuilder, PostgrestTransformBuilder, + PostgrestError, } // compatibility with CJS output @@ -22,4 +24,5 @@ export default { PostgrestFilterBuilder, PostgrestTransformBuilder, PostgrestBuilder, + PostgrestError, } From 2a729af62bbfafc1a046b0a89f24ffc3340fa1e1 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Sat, 23 Nov 2024 13:44:14 +0200 Subject: [PATCH 10/31] chore: rename type tets file --- .../{default-inference-d.ts => default-inference.test-d.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/select-query-parser/{default-inference-d.ts => default-inference.test-d.ts} (100%) diff --git a/test/select-query-parser/default-inference-d.ts b/test/select-query-parser/default-inference.test-d.ts similarity index 100% rename from test/select-query-parser/default-inference-d.ts rename to test/select-query-parser/default-inference.test-d.ts From c5111282404100496b6a77a83a6c515bd8b05fe7 Mon Sep 17 00:00:00 2001 From: Jordan Lewallen Date: Sat, 12 Oct 2024 13:16:31 -0700 Subject: [PATCH 11/31] fix: export unstable GetResult type --- src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.ts b/src/index.ts index 06ac6622..466a71bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,3 +29,6 @@ export type { PostgrestSingleResponse, PostgrestMaybeSingleResponse, } from './types' +// https://github.com/supabase/postgrest-js/issues/551 +// To be replaced with a helper type that only uses public types +export type { GetResult as UnstableGetResult } from './select-query-parser/result' From e556d3f83d1a3f1b79ec65be05c475cad3c2304f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Tue, 24 Dec 2024 14:36:57 +0100 Subject: [PATCH 12/31] fix: Correctly validate enum values in eq, neq and in methods --- src/PostgrestFilterBuilder.ts | 27 ++++++++++++--------------- test/index.test-d.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/PostgrestFilterBuilder.ts b/src/PostgrestFilterBuilder.ts index 21cc4090..59da806e 100644 --- a/src/PostgrestFilterBuilder.ts +++ b/src/PostgrestFilterBuilder.ts @@ -32,11 +32,6 @@ export default class PostgrestFilterBuilder< RelationName = unknown, Relationships = unknown > extends PostgrestTransformBuilder { - eq( - column: ColumnName, - value: NonNullable - ): this - eq(column: string, value: NonNullable): this /** * Match only rows where `column` is equal to `value`. * @@ -45,20 +40,24 @@ export default class PostgrestFilterBuilder< * @param column - The column to filter on * @param value - The value to filter with */ - eq(column: string, value: unknown): this { + eq( + column: ColumnName, + value: ColumnName extends keyof Row ? NonNullable : NonNullable + ): this { this.url.searchParams.append(column, `eq.${value}`) return this } - neq(column: ColumnName, value: Row[ColumnName]): this - neq(column: string, value: unknown): this /** * Match only rows where `column` is not equal to `value`. * * @param column - The column to filter on * @param value - The value to filter with */ - neq(column: string, value: unknown): this { + neq( + column: ColumnName, + value: ColumnName extends keyof Row ? Row[ColumnName] : unknown + ): this { this.url.searchParams.append(column, `neq.${value}`) return this } @@ -227,18 +226,16 @@ export default class PostgrestFilterBuilder< return this } - in( - column: ColumnName, - values: ReadonlyArray - ): this - in(column: string, values: readonly unknown[]): this /** * Match only rows where `column` is included in the `values` array. * * @param column - The column to filter on * @param values - The values array to filter with */ - in(column: string, values: readonly unknown[]): this { + in( + column: ColumnName, + values: ColumnName extends keyof Row ? ReadonlyArray : unknown[] + ): this { const cleanedValues = Array.from(new Set(values)) .map((s) => { // handle postgrest reserved characters diff --git a/test/index.test-d.ts b/test/index.test-d.ts index e64dabcc..34bb3f65 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -21,6 +21,40 @@ const postgrest = new PostgrestClient(REST_URL) expectError(postgrest.from('users').select().eq('username', nullableVar)) } +// `.eq()`, '.neq()' and `.in()` validate value when column is an enum +{ + expectError(postgrest.from('users').select().eq('status', 'invalid')) + expectError(postgrest.from('users').select().neq('status', 'invalid')) + expectError(postgrest.from('users').select().in('status', ['invalid'])) + + { + const { data, error } = await postgrest.from('users').select('status').eq('status', 'ONLINE') + if (error) { + throw new Error(error.message) + } + expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(data) + } + + { + const { data, error } = await postgrest.from('users').select('status').neq('status', 'ONLINE') + if (error) { + throw new Error(error.message) + } + expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(data) + } + + { + const { data, error } = await postgrest + .from('users') + .select('status') + .in('status', ['ONLINE', 'OFFLINE']) + if (error) { + throw new Error(error.message) + } + expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(data) + } +} + // can override result type { const { data, error } = await postgrest From 09083ea9d0a42baa36d672ba0735d15553b9cbf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Mon, 6 Jan 2025 16:03:18 +0100 Subject: [PATCH 13/31] fix: Correctly validate relationship enum values in eq, neq and in methods (#589) --- src/PostgrestFilterBuilder.ts | 43 ++++++++++++++++++++++--- test/index.test-d.ts | 59 ++++++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/src/PostgrestFilterBuilder.ts b/src/PostgrestFilterBuilder.ts index 59da806e..e6b658da 100644 --- a/src/PostgrestFilterBuilder.ts +++ b/src/PostgrestFilterBuilder.ts @@ -1,5 +1,5 @@ import PostgrestTransformBuilder from './PostgrestTransformBuilder' -import { GenericSchema } from './types' +import { GenericSchema, GenericTable } from './types' type FilterOperator = | 'eq' @@ -25,6 +25,35 @@ type FilterOperator = | 'phfts' | 'wfts' +// Match relationship filters with `table.column` syntax and resolve underlying +// column value. If not matched, fallback to generic type. +// TODO: Validate the relationship itself ala select-query-parser. Currently we +// assume that all tables have valid relationships to each other, despite +// nonexistent foreign keys. +type ResolveFilterValue< + Tables extends Record, + Row extends Record, + ColumnName extends string +> = ColumnName extends `${infer RelationshipTable}.${infer Remainder}` + ? Remainder extends `${infer _}.${infer _}` + ? ResolveFilterValue + : ResolveFilterRelationshipValue + : ColumnName extends keyof Row + ? Row[ColumnName] + : never + +type ResolveFilterRelationshipValue< + Tables extends Record, + RelationshipTable extends string, + RelationshipColumn extends string +> = RelationshipTable extends keyof Tables + ? 'Row' extends keyof Tables[RelationshipTable] + ? RelationshipColumn extends keyof Tables[RelationshipTable]['Row'] + ? Tables[RelationshipTable]['Row'][RelationshipColumn] + : unknown + : unknown + : unknown + export default class PostgrestFilterBuilder< Schema extends GenericSchema, Row extends Record, @@ -42,7 +71,9 @@ export default class PostgrestFilterBuilder< */ eq( column: ColumnName, - value: ColumnName extends keyof Row ? NonNullable : NonNullable + value: ResolveFilterValue extends never + ? NonNullable + : NonNullable> ): this { this.url.searchParams.append(column, `eq.${value}`) return this @@ -56,7 +87,9 @@ export default class PostgrestFilterBuilder< */ neq( column: ColumnName, - value: ColumnName extends keyof Row ? Row[ColumnName] : unknown + value: ResolveFilterValue extends never + ? unknown + : ResolveFilterValue ): this { this.url.searchParams.append(column, `neq.${value}`) return this @@ -234,7 +267,9 @@ export default class PostgrestFilterBuilder< */ in( column: ColumnName, - values: ColumnName extends keyof Row ? ReadonlyArray : unknown[] + values: ResolveFilterValue extends never + ? unknown[] + : ReadonlyArray> ): this { const cleanedValues = Array.from(new Set(values)) .map((s) => { diff --git a/test/index.test-d.ts b/test/index.test-d.ts index 34bb3f65..50a2444d 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -21,12 +21,36 @@ const postgrest = new PostgrestClient(REST_URL) expectError(postgrest.from('users').select().eq('username', nullableVar)) } -// `.eq()`, '.neq()' and `.in()` validate value when column is an enum +// `.eq()`, '.neq()' and `.in()` validate provided filter value when column is an enum. +// Behaves the same for simple columns, as well as relationship filters. { expectError(postgrest.from('users').select().eq('status', 'invalid')) expectError(postgrest.from('users').select().neq('status', 'invalid')) expectError(postgrest.from('users').select().in('status', ['invalid'])) + expectError( + postgrest.from('best_friends').select('users!first_user(status)').eq('users.status', 'invalid') + ) + expectError( + postgrest.from('best_friends').select('users!first_user(status)').neq('users.status', 'invalid') + ) + expectError( + postgrest + .from('best_friends') + .select('users!first_user(status)') + .in('users.status', ['invalid']) + ) + // Validate deeply nested embedded tables + expectError( + postgrest.from('users').select('messages(channels(*))').eq('messages.channels.id', 'invalid') + ) + expectError( + postgrest.from('users').select('messages(channels(*))').neq('messages.channels.id', 'invalid') + ) + expectError( + postgrest.from('users').select('messages(channels(*))').in('messages.channels.id', ['invalid']) + ) + { const { data, error } = await postgrest.from('users').select('status').eq('status', 'ONLINE') if (error) { @@ -53,6 +77,39 @@ const postgrest = new PostgrestClient(REST_URL) } expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(data) } + + { + const { data, error } = await postgrest + .from('best_friends') + .select('users!first_user(status)') + .eq('users.status', 'ONLINE') + if (error) { + throw new Error(error.message) + } + expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>(data) + } + + { + const { data, error } = await postgrest + .from('best_friends') + .select('users!first_user(status)') + .neq('users.status', 'ONLINE') + if (error) { + throw new Error(error.message) + } + expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>(data) + } + + { + const { data, error } = await postgrest + .from('best_friends') + .select('users!first_user(status)') + .in('users.status', ['ONLINE', 'OFFLINE']) + if (error) { + throw new Error(error.message) + } + expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>(data) + } } // can override result type From 8d32089ec4500968ee016b76cb0a1b14d37a990a Mon Sep 17 00:00:00 2001 From: Bobbie Soedirgo Date: Tue, 7 Jan 2025 23:55:12 +0800 Subject: [PATCH 14/31] fix: also validate enums in views --- src/PostgrestFilterBuilder.ts | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/PostgrestFilterBuilder.ts b/src/PostgrestFilterBuilder.ts index e6b658da..4dbee83c 100644 --- a/src/PostgrestFilterBuilder.ts +++ b/src/PostgrestFilterBuilder.ts @@ -1,5 +1,5 @@ import PostgrestTransformBuilder from './PostgrestTransformBuilder' -import { GenericSchema, GenericTable } from './types' +import { GenericSchema } from './types' type FilterOperator = | 'eq' @@ -31,28 +31,30 @@ type FilterOperator = // assume that all tables have valid relationships to each other, despite // nonexistent foreign keys. type ResolveFilterValue< - Tables extends Record, + Schema extends GenericSchema, Row extends Record, ColumnName extends string > = ColumnName extends `${infer RelationshipTable}.${infer Remainder}` ? Remainder extends `${infer _}.${infer _}` - ? ResolveFilterValue - : ResolveFilterRelationshipValue + ? ResolveFilterValue + : ResolveFilterRelationshipValue : ColumnName extends keyof Row ? Row[ColumnName] : never type ResolveFilterRelationshipValue< - Tables extends Record, + Schema extends GenericSchema, RelationshipTable extends string, RelationshipColumn extends string -> = RelationshipTable extends keyof Tables - ? 'Row' extends keyof Tables[RelationshipTable] - ? RelationshipColumn extends keyof Tables[RelationshipTable]['Row'] - ? Tables[RelationshipTable]['Row'][RelationshipColumn] +> = Schema['Tables'] & Schema['Views'] extends infer TablesAndViews + ? RelationshipTable extends keyof TablesAndViews + ? 'Row' extends keyof TablesAndViews[RelationshipTable] + ? RelationshipColumn extends keyof TablesAndViews[RelationshipTable]['Row'] + ? TablesAndViews[RelationshipTable]['Row'][RelationshipColumn] + : unknown : unknown : unknown - : unknown + : never export default class PostgrestFilterBuilder< Schema extends GenericSchema, @@ -71,9 +73,9 @@ export default class PostgrestFilterBuilder< */ eq( column: ColumnName, - value: ResolveFilterValue extends never + value: ResolveFilterValue extends never ? NonNullable - : NonNullable> + : NonNullable> ): this { this.url.searchParams.append(column, `eq.${value}`) return this @@ -87,9 +89,9 @@ export default class PostgrestFilterBuilder< */ neq( column: ColumnName, - value: ResolveFilterValue extends never + value: ResolveFilterValue extends never ? unknown - : ResolveFilterValue + : ResolveFilterValue ): this { this.url.searchParams.append(column, `neq.${value}`) return this @@ -267,9 +269,9 @@ export default class PostgrestFilterBuilder< */ in( column: ColumnName, - values: ResolveFilterValue extends never + values: ResolveFilterValue extends never ? unknown[] - : ReadonlyArray> + : ReadonlyArray> ): this { const cleanedValues = Array.from(new Set(values)) .map((s) => { From 8bdb9691cd2ec4ce9fd9227ed6abfcf3af1bf98c Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 13 Jan 2025 00:07:59 +0900 Subject: [PATCH 15/31] feat(types): add jsonpath parser --- src/select-query-parser/parser.ts | 17 ++++++++-- src/select-query-parser/utils.ts | 8 +++++ test/index.test-d.ts | 11 +++++++ test/select-query-parser/parser.test-d.ts | 39 ++++++++++++++++++----- test/types.ts | 14 ++++++-- 5 files changed, 76 insertions(+), 13 deletions(-) diff --git a/src/select-query-parser/parser.ts b/src/select-query-parser/parser.ts index a3944d73..c4336fe4 100644 --- a/src/select-query-parser/parser.ts +++ b/src/select-query-parser/parser.ts @@ -2,6 +2,7 @@ // See https://github.com/PostgREST/postgrest/blob/2f91853cb1de18944a4556df09e52450b881cfb3/src/PostgREST/ApiRequest/QueryParams.hs#L282-L284 import { SimplifyDeep } from '../types' +import { JsonPathToAccessor } from './utils' /** * Parses a query. @@ -220,13 +221,24 @@ type ParseNonEmbeddedResourceField = ParseIdentifier${infer _}` + Remainder extends `->${infer PathAndRest}` ? ParseJsonAccessor extends [ infer PropertyName, infer PropertyType, `${infer Remainder}` ] - ? [{ type: 'field'; name: Name; alias: PropertyName; castType: PropertyType }, Remainder] + ? [ + { + type: 'field' + name: Name + alias: PropertyName + castType: PropertyType + jsonPath: JsonPathToAccessor< + PathAndRest extends `${infer Path},${string}` ? Path : PathAndRest + > + }, + Remainder + ] : ParseJsonAccessor : [{ type: 'field'; name: Name }, Remainder] ) extends infer Parsed @@ -401,6 +413,7 @@ export namespace Ast { hint?: string innerJoin?: true castType?: string + jsonPath?: string aggregateFunction?: Token.AggregateFunction children?: Node[] } diff --git a/src/select-query-parser/utils.ts b/src/select-query-parser/utils.ts index b3691e93..14be31d4 100644 --- a/src/select-query-parser/utils.ts +++ b/src/select-query-parser/utils.ts @@ -544,3 +544,11 @@ export type FindFieldMatchingRelationships< name: Field['name'] } : SelectQueryError<'Failed to find matching relation via name'> + +export type JsonPathToAccessor = Path extends `${infer P1}->${infer P2}` + ? P2 extends `>${infer Rest}` // Check if P2 starts with > (from ->>) + ? JsonPathToAccessor<`${P1}.${Rest}`> + : JsonPathToAccessor<`${P1}.${P2}`> + : Path extends `${infer P1}::${infer _}` // Handle type casting + ? JsonPathToAccessor // Process the path without the cast type + : Path diff --git a/test/index.test-d.ts b/test/index.test-d.ts index 50a2444d..3e318615 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -211,3 +211,14 @@ const postgrest = new PostgrestClient(REST_URL) expectType(y) expectType(z) } + +// Json Accessor with custom types overrides +{ + const { error } = await postgrest + .schema('personal') + .from('users') + .select('data->foo->bar, data->foo->>baz') + if (error) { + throw new Error(error.message) + } +} diff --git a/test/select-query-parser/parser.test-d.ts b/test/select-query-parser/parser.test-d.ts index 8c7291ac..51789a99 100644 --- a/test/select-query-parser/parser.test-d.ts +++ b/test/select-query-parser/parser.test-d.ts @@ -81,17 +81,28 @@ import { selectParams } from '../relationships' // Select with JSON accessor { expectTypepreferences->theme'>>([ - { type: 'field', name: 'data', alias: 'theme', castType: 'json' }, + { + type: 'field', + name: 'data', + alias: 'theme', + castType: 'json', + jsonPath: 'preferences.theme', + }, ]) } // Select with JSON accessor and text conversion { expectTypepreferences->>theme'>>([ - { type: 'field', name: 'data', alias: 'theme', castType: 'text' }, + { + type: 'field', + name: 'data', + alias: 'theme', + castType: 'text', + jsonPath: 'preferences.theme', + }, ]) } - // Select with spread { expectType>([ @@ -196,7 +207,13 @@ import { selectParams } from '../relationships' }, ], }, - { type: 'field', name: 'profile', alias: 'theme', castType: 'text' }, + { + type: 'field', + name: 'profile', + alias: 'theme', + castType: 'text', + jsonPath: 'settings.theme', + }, ]) } { @@ -327,7 +344,13 @@ import { selectParams } from '../relationships' // Select with nested JSON accessors { expectTypepreferences->theme->color'>>([ - { type: 'field', name: 'data', alias: 'color', castType: 'json' }, + { + type: 'field', + name: 'data', + alias: 'color', + castType: 'json', + jsonPath: 'preferences.theme.color', + }, ]) } @@ -464,7 +487,7 @@ import { selectParams } from '../relationships' expectTypeage::int'>>([ { type: 'field', name: 'id', castType: 'text' }, { type: 'field', name: 'created_at', castType: 'date' }, - { type: 'field', name: 'data', alias: 'age', castType: 'int' }, + { type: 'field', name: 'data', alias: 'age', castType: 'int', jsonPath: 'age' }, ]) } @@ -480,8 +503,8 @@ import { selectParams } from '../relationships' // select JSON accessor { expect>([ - { type: 'field', name: 'data', alias: 'bar', castType: 'json' }, - { type: 'field', name: 'data', alias: 'baz', castType: 'text' }, + { type: 'field', name: 'data', alias: 'bar', castType: 'json', jsonPath: 'foo.bar' }, + { type: 'field', name: 'data', alias: 'baz', castType: 'text', jsonPath: 'foo.baz' }, ]) } diff --git a/test/types.ts b/test/types.ts index 2ed1d1c4..7a2954ef 100644 --- a/test/types.ts +++ b/test/types.ts @@ -1,24 +1,32 @@ export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[] +type CustomUserDataType = { + foo: string + bar: { + baz: number + } + en: 'ONE' | 'TWO' | 'THREE' +} + export type Database = { personal: { Tables: { users: { Row: { age_range: unknown | null - data: Json | null + data: CustomUserDataType | null status: Database['public']['Enums']['user_status'] | null username: string } Insert: { age_range?: unknown | null - data?: Json | null + data?: CustomUserDataType | null status?: Database['public']['Enums']['user_status'] | null username: string } Update: { age_range?: unknown | null - data?: Json | null + data?: CustomUserDataType | null status?: Database['public']['Enums']['user_status'] | null username?: string } From daa87054605b799e3b087c285bc19c88b426a5df Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 13 Jan 2025 01:17:21 +0900 Subject: [PATCH 16/31] feat(types): allow jsonpath selector types overrides --- src/select-query-parser/result.ts | 30 ++++++++++++++++++++-- src/select-query-parser/utils.ts | 30 +++++++++++++++++++--- test/index.test-d.ts | 30 ++++++++++++++++++++-- test/select-query-parser/parser.test-d.ts | 25 ++++++++++++++++++ test/select-query-parser/result.test-d.ts | 31 +++++++++++++++++++++++ 5 files changed, 139 insertions(+), 7 deletions(-) diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index 4af7d05e..c1cd42ef 100644 --- a/src/select-query-parser/result.ts +++ b/src/select-query-parser/result.ts @@ -15,6 +15,8 @@ import { GetFieldNodeResultName, IsAny, IsRelationNullable, + IsStringUnion, + JsonPathToType, ResolveRelationship, SelectQueryError, } from './utils' @@ -239,6 +241,30 @@ type ProcessFieldNode< ? ProcessEmbeddedResource : ProcessSimpleField +type ResolveJsonPathType< + Value, + Path extends string | undefined, + CastType extends PostgreSQLTypes +> = Path extends string + ? JsonPathToType extends never + ? // Always fallback if JsonPathToType returns never + TypeScriptTypes + : JsonPathToType extends infer PathResult + ? PathResult extends string + ? // Use the result if it's a string as we know that even with the string accessor ->> it's a valid type + PathResult + : IsStringUnion extends true + ? // Use the result if it's a union of strings + PathResult + : CastType extends 'json' + ? // If the type is not a string, ensure it was accessed with json accessor -> + PathResult + : // Otherwise it means non-string value accessed with string accessor ->> use the TypeScriptTypes result + TypeScriptTypes + : TypeScriptTypes + : // No json path, use regular type casting + TypeScriptTypes + /** * Processes a simple field (without embedded resources). * @@ -261,8 +287,8 @@ type ProcessSimpleField< } : { // Aliases override the property name in the result - [K in GetFieldNodeResultName]: Field['castType'] extends PostgreSQLTypes // We apply the detected casted as the result type - ? TypeScriptTypes + [K in GetFieldNodeResultName]: Field['castType'] extends PostgreSQLTypes + ? ResolveJsonPathType : Row[Field['name']] } : SelectQueryError<`column '${Field['name']}' does not exist on '${RelationName}'.`> diff --git a/src/select-query-parser/utils.ts b/src/select-query-parser/utils.ts index 14be31d4..dac15f08 100644 --- a/src/select-query-parser/utils.ts +++ b/src/select-query-parser/utils.ts @@ -546,9 +546,33 @@ export type FindFieldMatchingRelationships< : SelectQueryError<'Failed to find matching relation via name'> export type JsonPathToAccessor = Path extends `${infer P1}->${infer P2}` - ? P2 extends `>${infer Rest}` // Check if P2 starts with > (from ->>) + ? P2 extends `>${infer Rest}` // Handle ->> operator ? JsonPathToAccessor<`${P1}.${Rest}`> - : JsonPathToAccessor<`${P1}.${P2}`> + : P2 extends string // Handle -> operator + ? JsonPathToAccessor<`${P1}.${P2}`> + : Path + : Path extends `>${infer Rest}` // Clean up any remaining > characters + ? JsonPathToAccessor : Path extends `${infer P1}::${infer _}` // Handle type casting - ? JsonPathToAccessor // Process the path without the cast type + ? JsonPathToAccessor : Path + +export type JsonPathToType = Path extends '' + ? T + : ContainsNull extends true + ? JsonPathToType, Path> + : Path extends `${infer Key}.${infer Rest}` + ? Key extends keyof T + ? JsonPathToType + : never + : Path extends keyof T + ? T[Path] + : never + +export type IsStringUnion = string extends T + ? false + : T extends string + ? [T] extends [never] + ? false + : true + : false diff --git a/test/index.test-d.ts b/test/index.test-d.ts index 3e318615..93414db0 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -214,11 +214,37 @@ const postgrest = new PostgrestClient(REST_URL) // Json Accessor with custom types overrides { - const { error } = await postgrest + const { data, error } = await postgrest .schema('personal') .from('users') - .select('data->foo->bar, data->foo->>baz') + .select('data->bar->baz, data->en, data->bar') + if (error) { + throw new Error(error.message) + } + expectType< + { + baz: number + en: 'ONE' | 'TWO' | 'THREE' + bar: { + baz: number + } + }[] + >(data) +} +// Json string Accessor with custom types overrides +{ + const { data, error } = await postgrest + .schema('personal') + .from('users') + .select('data->bar->>baz, data->>en, data->>bar') if (error) { throw new Error(error.message) } + expectType< + { + baz: string + en: 'ONE' | 'TWO' | 'THREE' + bar: string + }[] + >(data) } diff --git a/test/select-query-parser/parser.test-d.ts b/test/select-query-parser/parser.test-d.ts index 51789a99..759f9724 100644 --- a/test/select-query-parser/parser.test-d.ts +++ b/test/select-query-parser/parser.test-d.ts @@ -103,6 +103,31 @@ import { selectParams } from '../relationships' }, ]) } +{ + expectTypepreferences->>theme, data->>some, data->foo->bar->>biz'>>([ + { + type: 'field', + name: 'data', + alias: 'theme', + castType: 'text', + jsonPath: 'preferences.theme', + }, + { + type: 'field', + name: 'data', + alias: 'some', + castType: 'text', + jsonPath: 'some', + }, + { + type: 'field', + name: 'data', + alias: 'biz', + castType: 'text', + jsonPath: 'foo.bar.biz', + }, + ]) +} // Select with spread { expectType>([ diff --git a/test/select-query-parser/result.test-d.ts b/test/select-query-parser/result.test-d.ts index 74a40836..89615ba7 100644 --- a/test/select-query-parser/result.test-d.ts +++ b/test/select-query-parser/result.test-d.ts @@ -118,3 +118,34 @@ type SelectQueryFromTableResult< expectType(result2!) expectType(result3!) } + +{ + type SelectQueryFromTableResult< + TableName extends keyof Database['personal']['Tables'], + Q extends string + > = GetResult< + Database['personal'], + Database['personal']['Tables'][TableName]['Row'], + TableName, + Database['personal']['Tables'][TableName]['Relationships'], + Q + > + + let result: SelectQueryFromTableResult<'users', `data->bar->baz, data->en, data->bar`> + let expected: { + baz: number + en: 'ONE' | 'TWO' | 'THREE' + bar: { + baz: number + } + } + expectType>(true) + + let result2: SelectQueryFromTableResult<'users', `data->bar->>baz, data->>en, data->>bar`> + let expected2: { + baz: string + en: 'ONE' | 'TWO' | 'THREE' + bar: string + } + expectType>(true) +} From a27eb9e7a0d13abf0da20ce5d0ee98cb41a68b7a Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 13 Jan 2025 13:32:31 +0900 Subject: [PATCH 17/31] chore: add test for fallback --- test/select-query-parser/result.test-d.ts | 46 +++++++++++++++-------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/test/select-query-parser/result.test-d.ts b/test/select-query-parser/result.test-d.ts index 89615ba7..0e4c541a 100644 --- a/test/select-query-parser/result.test-d.ts +++ b/test/select-query-parser/result.test-d.ts @@ -120,7 +120,7 @@ type SelectQueryFromTableResult< } { - type SelectQueryFromTableResult< + type SelectQueryFromPersonalTableResult< TableName extends keyof Database['personal']['Tables'], Q extends string > = GetResult< @@ -130,22 +130,38 @@ type SelectQueryFromTableResult< Database['personal']['Tables'][TableName]['Relationships'], Q > - - let result: SelectQueryFromTableResult<'users', `data->bar->baz, data->en, data->bar`> - let expected: { - baz: number - en: 'ONE' | 'TWO' | 'THREE' - bar: { + // Should work with Json object accessor + { + let result: SelectQueryFromPersonalTableResult<'users', `data->bar->baz, data->en, data->bar`> + let expected: { baz: number + en: 'ONE' | 'TWO' | 'THREE' + bar: { + baz: number + } } + expectType>(true) } - expectType>(true) - - let result2: SelectQueryFromTableResult<'users', `data->bar->>baz, data->>en, data->>bar`> - let expected2: { - baz: string - en: 'ONE' | 'TWO' | 'THREE' - bar: string + // Should work with Json string accessor + { + let result: SelectQueryFromPersonalTableResult< + 'users', + `data->bar->>baz, data->>en, data->>bar` + > + let expected: { + baz: string + en: 'ONE' | 'TWO' | 'THREE' + bar: string + } + expectType>(true) + } + // Should fallback to defaults if unknown properties are mentionned + { + let result: SelectQueryFromPersonalTableResult<'users', `data->bar->>nope, data->neither`> + let expected: { + nope: string + neither: Json + } + expectType>(true) } - expectType>(true) } From d30623264b8365bf89bc25d14df9aa7e82580c4e Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 13 Jan 2025 13:40:43 +0900 Subject: [PATCH 18/31] chore: fix tests null result case --- test/index.test-d.ts | 134 +++++++++++++++++++++---------------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/test/index.test-d.ts b/test/index.test-d.ts index 93414db0..db1c8969 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -52,87 +52,93 @@ const postgrest = new PostgrestClient(REST_URL) ) { - const { data, error } = await postgrest.from('users').select('status').eq('status', 'ONLINE') - if (error) { - throw new Error(error.message) + const result = await postgrest.from('users').select('status').eq('status', 'ONLINE') + if (result.error) { + throw new Error(result.error.message) } - expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(data) + expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(result.data) } { - const { data, error } = await postgrest.from('users').select('status').neq('status', 'ONLINE') - if (error) { - throw new Error(error.message) + const result = await postgrest.from('users').select('status').neq('status', 'ONLINE') + if (result.error) { + throw new Error(result.error.message) } - expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(data) + expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(result.data) } { - const { data, error } = await postgrest + const result = await postgrest .from('users') .select('status') .in('status', ['ONLINE', 'OFFLINE']) - if (error) { - throw new Error(error.message) + if (result.error) { + throw new Error(result.error.message) } - expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(data) + expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(result.data) } { - const { data, error } = await postgrest + const result = await postgrest .from('best_friends') .select('users!first_user(status)') .eq('users.status', 'ONLINE') - if (error) { - throw new Error(error.message) + if (result.error) { + throw new Error(result.error.message) } - expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>(data) + expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>( + result.data + ) } { - const { data, error } = await postgrest + const result = await postgrest .from('best_friends') .select('users!first_user(status)') .neq('users.status', 'ONLINE') - if (error) { - throw new Error(error.message) + if (result.error) { + throw new Error(result.error.message) } - expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>(data) + expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>( + result.data + ) } { - const { data, error } = await postgrest + const result = await postgrest .from('best_friends') .select('users!first_user(status)') .in('users.status', ['ONLINE', 'OFFLINE']) - if (error) { - throw new Error(error.message) + if (result.error) { + throw new Error(result.error.message) } - expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>(data) + expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>( + result.data + ) } } // can override result type { - const { data, error } = await postgrest + const result = await postgrest .from('users') .select('*, messages(*)') .returns<{ messages: { foo: 'bar' }[] }[]>() - if (error) { - throw new Error(error.message) + if (result.error) { + throw new Error(result.error.message) } - expectType<{ foo: 'bar' }[]>(data[0].messages) + expectType<{ foo: 'bar' }[]>(result.data[0].messages) } { - const { data, error } = await postgrest + const result = await postgrest .from('users') .insert({ username: 'foo' }) .select('*, messages(*)') .returns<{ messages: { foo: 'bar' }[] }[]>() - if (error) { - throw new Error(error.message) + if (result.error) { + throw new Error(result.error.message) } - expectType<{ foo: 'bar' }[]>(data[0].messages) + expectType<{ foo: 'bar' }[]>(result.data[0].messages) } // cannot update non-updatable views @@ -147,60 +153,54 @@ const postgrest = new PostgrestClient(REST_URL) // spread resource with single column in select query { - const { data, error } = await postgrest - .from('messages') - .select('message, ...users(status)') - .single() - if (error) { - throw new Error(error.message) + const result = await postgrest.from('messages').select('message, ...users(status)').single() + if (result.error) { + throw new Error(result.error.message) } expectType<{ message: string | null; status: Database['public']['Enums']['user_status'] | null }>( - data + result.data ) } // spread resource with all columns in select query { - const { data, error } = await postgrest.from('messages').select('message, ...users(*)').single() - if (error) { - throw new Error(error.message) + const result = await postgrest.from('messages').select('message, ...users(*)').single() + if (result.error) { + throw new Error(result.error.message) } expectType>( - data + result.data ) } // `count` in embedded resource { - const { data, error } = await postgrest.from('messages').select('message, users(count)').single() - if (error) { - throw new Error(error.message) + const result = await postgrest.from('messages').select('message, users(count)').single() + if (result.error) { + throw new Error(result.error.message) } - expectType<{ message: string | null; users: { count: number } }>(data) + expectType<{ message: string | null; users: { count: number } }>(result.data) } // json accessor in select query { - const { data, error } = await postgrest - .from('users') - .select('data->foo->bar, data->foo->>baz') - .single() - if (error) { - throw new Error(error.message) + const result = await postgrest.from('users').select('data->foo->bar, data->foo->>baz').single() + if (result.error) { + throw new Error(result.error.message) } // getting this w/o the cast, not sure why: // Parameter type Json is declared too wide for argument type Json - expectType(data.bar) - expectType(data.baz) + expectType(result.data.bar) + expectType(result.data.baz) } // rpc return type { - const { data, error } = await postgrest.rpc('get_status') - if (error) { - throw new Error(error.message) + const result = await postgrest.rpc('get_status') + if (result.error) { + throw new Error(result.error.message) } - expectType<'ONLINE' | 'OFFLINE'>(data) + expectType<'ONLINE' | 'OFFLINE'>(result.data) } // PostgrestBuilder's children retains class when using inherited methods @@ -214,12 +214,12 @@ const postgrest = new PostgrestClient(REST_URL) // Json Accessor with custom types overrides { - const { data, error } = await postgrest + const result = await postgrest .schema('personal') .from('users') .select('data->bar->baz, data->en, data->bar') - if (error) { - throw new Error(error.message) + if (result.error) { + throw new Error(result.error.message) } expectType< { @@ -229,16 +229,16 @@ const postgrest = new PostgrestClient(REST_URL) baz: number } }[] - >(data) + >(result.data) } // Json string Accessor with custom types overrides { - const { data, error } = await postgrest + const result = await postgrest .schema('personal') .from('users') .select('data->bar->>baz, data->>en, data->>bar') - if (error) { - throw new Error(error.message) + if (result.error) { + throw new Error(result.error.message) } expectType< { @@ -246,5 +246,5 @@ const postgrest = new PostgrestClient(REST_URL) en: 'ONE' | 'TWO' | 'THREE' bar: string }[] - >(data) + >(result.data) } From 12faa3df5a400f3ff8d3be1ffce13dc11b2aeaf2 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Tue, 14 Jan 2025 19:01:27 +0900 Subject: [PATCH 19/31] fix(types): type result for throwOnError responses (#590) * fix(types): type result for throwOnError responses When using throwOnError(), the response type is now more strictly typed: - Data is guaranteed to be non-null - Error field is removed from response type - Response type is controlled by generic ThrowOnError boolean parameter Fixes #563 * chore: re-use generic types * fix: return this to comply with PostgresFilterBuilder * chore: fix test to check inheritance and not equality --- src/PostgrestBuilder.ts | 26 ++++++++---- src/PostgrestTransformBuilder.ts | 12 +++--- test/index.test-d.ts | 71 ++++++++++++++++++++++++++++++-- 3 files changed, 93 insertions(+), 16 deletions(-) diff --git a/src/PostgrestBuilder.ts b/src/PostgrestBuilder.ts index 621785c9..2c3863e2 100644 --- a/src/PostgrestBuilder.ts +++ b/src/PostgrestBuilder.ts @@ -1,11 +1,14 @@ // @ts-ignore import nodeFetch from '@supabase/node-fetch' -import type { Fetch, PostgrestSingleResponse } from './types' +import type { Fetch, PostgrestSingleResponse, PostgrestResponseSuccess } from './types' import PostgrestError from './PostgrestError' -export default abstract class PostgrestBuilder - implements PromiseLike> +export default abstract class PostgrestBuilder + implements + PromiseLike< + ThrowOnError extends true ? PostgrestResponseSuccess : PostgrestSingleResponse + > { protected method: 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'DELETE' protected url: URL @@ -42,9 +45,9 @@ export default abstract class PostgrestBuilder * * {@link https://github.com/supabase/supabase-js/issues/92} */ - throwOnError(): this { + throwOnError(): this & PostgrestBuilder { this.shouldThrowOnError = true - return this + return this as this & PostgrestBuilder } /** @@ -56,9 +59,18 @@ export default abstract class PostgrestBuilder return this } - then, TResult2 = never>( + then< + TResult1 = ThrowOnError extends true + ? PostgrestResponseSuccess + : PostgrestSingleResponse, + TResult2 = never + >( onfulfilled?: - | ((value: PostgrestSingleResponse) => TResult1 | PromiseLike) + | (( + value: ThrowOnError extends true + ? PostgrestResponseSuccess + : PostgrestSingleResponse + ) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null diff --git a/src/PostgrestTransformBuilder.ts b/src/PostgrestTransformBuilder.ts index 4791702c..2be085c8 100644 --- a/src/PostgrestTransformBuilder.ts +++ b/src/PostgrestTransformBuilder.ts @@ -192,7 +192,7 @@ export default class PostgrestTransformBuilder< ResultOne = Result extends (infer ResultOne)[] ? ResultOne : never >(): PostgrestBuilder { this.headers['Accept'] = 'application/vnd.pgrst.object+json' - return this as PostgrestBuilder + return this as unknown as PostgrestBuilder } /** @@ -212,7 +212,7 @@ export default class PostgrestTransformBuilder< this.headers['Accept'] = 'application/vnd.pgrst.object+json' } this.isMaybeSingle = true - return this as PostgrestBuilder + return this as unknown as PostgrestBuilder } /** @@ -220,7 +220,7 @@ export default class PostgrestTransformBuilder< */ csv(): PostgrestBuilder { this.headers['Accept'] = 'text/csv' - return this as PostgrestBuilder + return this as unknown as PostgrestBuilder } /** @@ -228,7 +228,7 @@ export default class PostgrestTransformBuilder< */ geojson(): PostgrestBuilder> { this.headers['Accept'] = 'application/geo+json' - return this as PostgrestBuilder> + return this as unknown as PostgrestBuilder> } /** @@ -285,8 +285,8 @@ export default class PostgrestTransformBuilder< this.headers[ 'Accept' ] = `application/vnd.pgrst.plan+${format}; for="${forMediatype}"; options=${options};` - if (format === 'json') return this as PostgrestBuilder[]> - else return this as PostgrestBuilder + if (format === 'json') return this as unknown as PostgrestBuilder[]> + else return this as unknown as PostgrestBuilder } /** diff --git a/test/index.test-d.ts b/test/index.test-d.ts index 50a2444d..745b8c40 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -1,5 +1,6 @@ +import { TypeEqual } from 'ts-expect' import { expectError, expectType } from 'tsd' -import { PostgrestClient } from '../src/index' +import { PostgrestClient, PostgrestError } from '../src/index' import { Prettify } from '../src/types' import { Database, Json } from './types' @@ -208,6 +209,70 @@ const postgrest = new PostgrestClient(REST_URL) const x = postgrest.from('channels').select() const y = x.throwOnError() const z = x.setHeader('', '') - expectType(y) - expectType(z) + expectType(true) + expectType(true) +} + +// Should have nullable data and error field +{ + const result = await postgrest.from('users').select('username, messages(id, message)').limit(1) + let expected: + | { + username: string + messages: { + id: number + message: string | null + }[] + }[] + | null + const { data } = result + const { error } = result + expectType>(true) + let err: PostgrestError | null + expectType>(true) +} + +// Should have non nullable data and no error fields if throwOnError is added +{ + const result = await postgrest + .from('users') + .select('username, messages(id, message)') + .limit(1) + .throwOnError() + const { data } = result + const { error } = result + let expected: + | { + username: string + messages: { + id: number + message: string | null + }[] + }[] + expectType>(true) + expectType>(true) + error +} + +// Should work with throwOnError middle of the chaining +{ + const result = await postgrest + .from('users') + .select('username, messages(id, message)') + .throwOnError() + .eq('username', 'test') + .limit(1) + const { data } = result + const { error } = result + let expected: + | { + username: string + messages: { + id: number + message: string | null + }[] + }[] + expectType>(true) + expectType>(true) + error } From 178fc719008f1c64c9da41fa354570083c49825c Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 16 Jan 2025 09:45:50 +0900 Subject: [PATCH 20/31] chore: add test coverage to postgrest-js --- .github/workflows/ci.yml | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 469adac6..4412210f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,3 +23,6 @@ jobs: - run: | npm clean-install npm run test + + - name: Coveralls + uses: coverallsapp/github-action@v2 diff --git a/package.json b/package.json index e440b5ab..ab56b8a7 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "docs": "typedoc src/index.ts --out docs/v2", "docs:json": "typedoc --json docs/v2/spec.json --excludeExternals src/index.ts", "test": "run-s format:check test:types db:clean db:run test:run db:clean && node test/smoke.cjs && node test/smoke.mjs", - "test:run": "jest --runInBand", + "test:run": "jest --runInBand --coverage", "test:update": "run-s db:clean db:run && jest --runInBand --updateSnapshot && run-s db:clean", "test:types": "run-s build && tsd --files 'test/**/*.test-d.ts'", "db:clean": "cd test/db && docker compose down --volumes", From 633991c7ea0a83c3eeab1c9c7b02b275966c1ec9 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Thu, 16 Jan 2025 18:32:38 +0900 Subject: [PATCH 21/31] chore(test): add additional tests improving test coverage (#595) --- test/basic.ts | 78 ++++++++++++++++++++++++++++ test/db/00-schema.sql | 7 +++ test/db/01-dummy-data.sql | 6 +++ test/filters.ts | 105 ++++++++++++++++++++++++++++++++++++++ test/transforms.ts | 31 +++++++++++ test/types.ts | 18 +++++++ 6 files changed, 245 insertions(+) diff --git a/test/basic.ts b/test/basic.ts index 44daf354..5e578473 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -60,6 +60,62 @@ test('basic select table', async () => { `) }) +test('basic select returns types override', async () => { + const res = await postgrest.from('users').select().returns<{ status: 'ONLINE' | 'OFFLINE' }>() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "age_range": "[1,2)", + "catchphrase": "'cat' 'fat'", + "data": null, + "status": "ONLINE", + "username": "supabot", + }, + Object { + "age_range": "[25,35)", + "catchphrase": "'bat' 'cat'", + "data": null, + "status": "OFFLINE", + "username": "kiwicopple", + }, + Object { + "age_range": "[25,35)", + "catchphrase": "'bat' 'rat'", + "data": null, + "status": "ONLINE", + "username": "awailas", + }, + Object { + "age_range": "[20,30)", + "catchphrase": "'fat' 'rat'", + "data": null, + "status": "ONLINE", + "username": "dragarcia", + }, + Object { + "age_range": "[20,30)", + "catchphrase": "'json' 'test'", + "data": Object { + "foo": Object { + "bar": Object { + "nested": "value", + }, + "baz": "string value", + }, + }, + "status": "ONLINE", + "username": "jsonuser", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + test('basic select view', async () => { const res = await postgrest.from('updatable_view').select() expect(res).toMatchInlineSnapshot(` @@ -546,6 +602,28 @@ describe('basic insert, update, delete', () => { `) }) + test('insert quoted column', async () => { + let res = await postgrest + .from('cornercase') + .insert([{ 'column whitespace': 'foo', id: 1 }]) + .select('"column whitespace", id ') + + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "23505", + "details": "Key (id)=(1) already exists.", + "hint": null, + "message": "duplicate key value violates unique constraint \\"cornercase_pkey\\"", + }, + "status": 409, + "statusText": "Conflict", + } + `) + }) + test('basic update', async () => { let res = await postgrest .from('messages') diff --git a/test/db/00-schema.sql b/test/db/00-schema.sql index 07170703..ee3f6e2e 100644 --- a/test/db/00-schema.sql +++ b/test/db/00-schema.sql @@ -156,3 +156,10 @@ $$ language sql immutable; create function public.function_with_array_param(param uuid[]) returns void as '' language sql immutable; + + +create table public.cornercase ( + id int primary key, + "column whitespace" text, + array_column text[] +); diff --git a/test/db/01-dummy-data.sql b/test/db/01-dummy-data.sql index 8a2343eb..1ad59ca8 100644 --- a/test/db/01-dummy-data.sql +++ b/test/db/01-dummy-data.sql @@ -81,3 +81,9 @@ VALUES (2, 1), -- Smartphone is in Electronics (3, 1), -- Headphones are in Electronics (3, 3); -- Headphones are also in Audio + +INSERT INTO public.cornercase (id, array_column) +VALUES + (1, ARRAY['test', 'one']), + (2, ARRAY['another']), + (3, ARRAY['test2']); \ No newline at end of file diff --git a/test/filters.ts b/test/filters.ts index 8348f0a4..3338ec37 100644 --- a/test/filters.ts +++ b/test/filters.ts @@ -370,6 +370,56 @@ test('contains', async () => { `) }) +test('contains with json', async () => { + const res = await postgrest + .from('users') + .select('data') + .contains('data', { foo: { baz: 'string value' } }) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "data": Object { + "foo": Object { + "bar": Object { + "nested": "value", + }, + "baz": "string value", + }, + }, + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('contains with array', async () => { + const res = await postgrest + .from('cornercase') + .select('array_column') + .contains('array_column', ['test']) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "array_column": Array [ + "test", + "one", + ], + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + test('containedBy', async () => { const res = await postgrest.from('users').select('age_range').containedBy('age_range', '[1,2)') expect(res).toMatchInlineSnapshot(` @@ -387,6 +437,38 @@ test('containedBy', async () => { `) }) +test('containedBy with json', async () => { + const res = await postgrest + .from('users') + .select('data') + .containedBy('data', { foo: { baz: 'string value' } }) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [], + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('containedBy with array', async () => { + const res = await postgrest + .from('cornercase') + .select('array_column') + .containedBy('array_column', ['test']) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [], + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + test('rangeLt', async () => { const res = await postgrest.from('users').select('age_range').rangeLt('age_range', '[2,25)') expect(res).toMatchInlineSnapshot(` @@ -510,6 +592,29 @@ test('overlaps', async () => { `) }) +test('overlaps with array', async () => { + const res = await postgrest + .from('cornercase') + .select('array_column') + .overlaps('array_column', ['test']) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "array_column": Array [ + "test", + "one", + ], + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + test('textSearch', async () => { const res = await postgrest .from('users') diff --git a/test/transforms.ts b/test/transforms.ts index e6518910..10892d8f 100644 --- a/test/transforms.ts +++ b/test/transforms.ts @@ -294,6 +294,37 @@ test('csv', async () => { `) }) +test('geojson', async () => { + const res = await postgrest.from('shops').select().geojson() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "features": Array [ + Object { + "geometry": Object { + "coordinates": Array [ + -71.10044, + 42.373695, + ], + "type": "Point", + }, + "properties": Object { + "address": "1369 Cambridge St", + "id": 1, + }, + "type": "Feature", + }, + ], + "type": "FeatureCollection", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + test('abort signal', async () => { const ac = new AbortController() as globalThis.AbortController ac.abort() diff --git a/test/types.ts b/test/types.ts index 2ed1d1c4..8f84eced 100644 --- a/test/types.ts +++ b/test/types.ts @@ -400,6 +400,24 @@ export type Database = { } Relationships: [] } + cornercase: { + Row: { + 'column whitespace': string | null + array_column: unknown | null + id: number + } + Insert: { + 'column whitespace'?: string | null + array_column?: unknown | null + id: number + } + Update: { + 'column whitespace'?: string | null + array_column?: unknown | null + id?: number + } + Relationships: [] + } users: { Row: { age_range: unknown | null From a9842b27240948a1dbe3fb602af597f13584a951 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 20 Jan 2025 11:25:27 +0900 Subject: [PATCH 22/31] fix: jsonpath with embeded tables --- src/select-query-parser/utils.ts | 2 ++ test/basic.ts | 13 ++++++--- test/select-query-parser/parser.test-d.ts | 33 +++++++++++++++++++++++ test/select-query-parser/result.test-d.ts | 29 ++++++++++++++++++++ test/select-query-parser/select.test-d.ts | 4 +-- test/types.ts | 8 +++--- 6 files changed, 80 insertions(+), 9 deletions(-) diff --git a/src/select-query-parser/utils.ts b/src/select-query-parser/utils.ts index dac15f08..bcbfef04 100644 --- a/src/select-query-parser/utils.ts +++ b/src/select-query-parser/utils.ts @@ -555,6 +555,8 @@ export type JsonPathToAccessor = Path extends `${infer P1}- ? JsonPathToAccessor : Path extends `${infer P1}::${infer _}` // Handle type casting ? JsonPathToAccessor + : Path extends `${infer P1}${')' | ','}${infer _}` // Handle closing parenthesis and comma + ? P1 : Path export type JsonPathToType = Path extends '' diff --git a/test/basic.ts b/test/basic.ts index 44daf354..60f9383b 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -1,5 +1,5 @@ import { PostgrestClient } from '../src/index' -import { Database } from './types' +import { CustomUserDataType, Database } from './types' const REST_URL = 'http://localhost:3000' const postgrest = new PostgrestClient(REST_URL) @@ -1615,7 +1615,10 @@ test('select with no match', async () => { }) test('update with no match - return=minimal', async () => { - const res = await postgrest.from('users').update({ data: '' }).eq('username', 'missing') + const res = await postgrest + .from('users') + .update({ data: '' as unknown as CustomUserDataType }) + .eq('username', 'missing') expect(res).toMatchInlineSnapshot(` Object { "count": null, @@ -1628,7 +1631,11 @@ test('update with no match - return=minimal', async () => { }) test('update with no match - return=representation', async () => { - const res = await postgrest.from('users').update({ data: '' }).eq('username', 'missing').select() + const res = await postgrest + .from('users') + .update({ data: '' as unknown as CustomUserDataType }) + .eq('username', 'missing') + .select() expect(res).toMatchInlineSnapshot(` Object { "count": null, diff --git a/test/select-query-parser/parser.test-d.ts b/test/select-query-parser/parser.test-d.ts index 759f9724..b241e13d 100644 --- a/test/select-query-parser/parser.test-d.ts +++ b/test/select-query-parser/parser.test-d.ts @@ -662,3 +662,36 @@ import { selectParams } from '../relationships' 0 as any as ParserError<'Unexpected input: ->->theme'> ) } + +// JSON accessor within embedded tables +{ + expectTypebar->>baz, data->>en, data->bar)'>>([ + { + type: 'field', + name: 'users', + children: [ + { + type: 'field', + name: 'data', + alias: 'baz', + castType: 'text', + jsonPath: 'bar.baz', + }, + { + type: 'field', + name: 'data', + alias: 'en', + castType: 'text', + jsonPath: 'en', + }, + { + type: 'field', + name: 'data', + alias: 'bar', + castType: 'json', + jsonPath: 'bar', + }, + ], + }, + ]) +} diff --git a/test/select-query-parser/result.test-d.ts b/test/select-query-parser/result.test-d.ts index 0e4c541a..508424f0 100644 --- a/test/select-query-parser/result.test-d.ts +++ b/test/select-query-parser/result.test-d.ts @@ -164,4 +164,33 @@ type SelectQueryFromTableResult< } expectType>(true) } + // Should work with embeded Json object accessor + { + let result: SelectQueryFromTableResult<'messages', `users(data->bar->baz, data->en, data->bar)`> + let expected: { + users: { + baz: number + en: 'ONE' | 'TWO' | 'THREE' + bar: { + baz: number + } + } + } + expectType>(true) + } + // Should work with embeded Json string accessor + { + let result: SelectQueryFromTableResult< + 'messages', + `users(data->bar->>baz, data->>en, data->>bar)` + > + let expected: { + users: { + baz: string + en: 'ONE' | 'TWO' | 'THREE' + bar: string + } + } + expectType>(true) + } } diff --git a/test/select-query-parser/select.test-d.ts b/test/select-query-parser/select.test-d.ts index 2343fa6f..adc54416 100644 --- a/test/select-query-parser/select.test-d.ts +++ b/test/select-query-parser/select.test-d.ts @@ -3,7 +3,7 @@ import { TypeEqual } from 'ts-expect' import { Json } from '../../src/select-query-parser/types' import { SelectQueryError } from '../../src/select-query-parser/utils' import { Prettify } from '../../src/types' -import { Database } from '../types' +import { CustomUserDataType, Database } from '../types' import { selectQueries } from '../relationships' // This test file is here to ensure that for a query against a specfic datatabase @@ -617,7 +617,7 @@ type Schema = Database['public'] users: { age_range: unknown | null catchphrase: unknown | null - data: Json | null + data: CustomUserDataType | null status: Database['public']['Enums']['user_status'] | null username: string } diff --git a/test/types.ts b/test/types.ts index 7a2954ef..ba4366e3 100644 --- a/test/types.ts +++ b/test/types.ts @@ -1,6 +1,6 @@ export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[] -type CustomUserDataType = { +export type CustomUserDataType = { foo: string bar: { baz: number @@ -412,21 +412,21 @@ export type Database = { Row: { age_range: unknown | null catchphrase: unknown | null - data: Json | null + data: CustomUserDataType | null status: Database['public']['Enums']['user_status'] | null username: string } Insert: { age_range?: unknown | null catchphrase?: unknown | null - data?: Json | null + data?: CustomUserDataType | null status?: Database['public']['Enums']['user_status'] | null username: string } Update: { age_range?: unknown | null catchphrase?: unknown | null - data?: Json | null + data?: CustomUserDataType | null status?: Database['public']['Enums']['user_status'] | null username?: string } From 9f21996d4e03935270193669606270c5286a8d93 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 20 Jan 2025 18:00:50 +0900 Subject: [PATCH 23/31] wip: reproduce https://github.com/supabase/supabase-js/issues/1354 --- package-lock.json | 37 ++++++-- package.json | 1 + test/issue-1354-d.ts | 215 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 246 insertions(+), 7 deletions(-) create mode 100644 test/issue-1354-d.ts diff --git a/package-lock.json b/package-lock.json index 55395c0b..a78cf6f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "ts-expect": "^1.3.0", "ts-jest": "^28.0.3", "tsd": "^0.31.2", + "type-fest": "^4.32.0", "typedoc": "^0.22.16", "typescript": "4.5.5", "wait-for-localhost-cli": "^3.0.0" @@ -1315,6 +1316,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -5904,12 +5918,13 @@ } }, "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.32.0.tgz", + "integrity": "sha512-rfgpoi08xagF3JSdtJlCwMq9DGNDE0IMh3Mkpc1wUypg9vPi786AiqeBBKcqvIkq42azsBM85N490fyZjeUftw==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=10" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7224,6 +7239,14 @@ "dev": true, "requires": { "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + } } }, "ansi-regex": { @@ -10554,9 +10577,9 @@ "dev": true }, "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.32.0.tgz", + "integrity": "sha512-rfgpoi08xagF3JSdtJlCwMq9DGNDE0IMh3Mkpc1wUypg9vPi786AiqeBBKcqvIkq42azsBM85N490fyZjeUftw==", "dev": true }, "typedoc": { diff --git a/package.json b/package.json index ab56b8a7..5b2d4233 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "ts-expect": "^1.3.0", "ts-jest": "^28.0.3", "tsd": "^0.31.2", + "type-fest": "^4.32.0", "typedoc": "^0.22.16", "typescript": "4.5.5", "wait-for-localhost-cli": "^3.0.0" diff --git a/test/issue-1354-d.ts b/test/issue-1354-d.ts new file mode 100644 index 00000000..b9840a0a --- /dev/null +++ b/test/issue-1354-d.ts @@ -0,0 +1,215 @@ +import { expectType } from 'tsd' +import { PostgrestClient } from '../src/index' +import type { MergeDeep } from 'type-fest' + +export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[] + +export type Database = { + public: { + Tables: { + foo: { + Row: { + created_at: string | null + bar: Json + id: string + baz: Json + game_id: string + updated_at: string | null + user_id: string | null + } + Insert: { + created_at?: string | null + bar: Json + id?: string + baz: Json + game_id: string + updated_at?: string | null + user_id?: string | null + } + Update: { + created_at?: string | null + bar?: Json + id?: string + baz?: Json + game_id?: string + updated_at?: string | null + user_id?: string | null + } + Relationships: [] + } + } + Views: {} + Functions: {} + Enums: {} + CompositeTypes: { + [_ in never]: never + } + } +} + +type PublicSchema = Database[Extract] + +export type Tables< + PublicTableNameOrOptions extends + | keyof (PublicSchema['Tables'] & PublicSchema['Views']) + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof (Database[PublicTableNameOrOptions['schema']]['Tables'] & + Database[PublicTableNameOrOptions['schema']]['Views']) + : never = never +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? (Database[PublicTableNameOrOptions['schema']]['Tables'] & + Database[PublicTableNameOrOptions['schema']]['Views'])[TableName] extends { + Row: infer R + } + ? R + : never + : PublicTableNameOrOptions extends keyof (PublicSchema['Tables'] & PublicSchema['Views']) + ? (PublicSchema['Tables'] & PublicSchema['Views'])[PublicTableNameOrOptions] extends { + Row: infer R + } + ? R + : never + : never + +export type TablesInsert< + PublicTableNameOrOptions extends keyof PublicSchema['Tables'] | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions['schema']]['Tables'] + : never = never +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends { + Insert: infer I + } + ? I + : never + : PublicTableNameOrOptions extends keyof PublicSchema['Tables'] + ? PublicSchema['Tables'][PublicTableNameOrOptions] extends { + Insert: infer I + } + ? I + : never + : never + +export type TablesUpdate< + PublicTableNameOrOptions extends keyof PublicSchema['Tables'] | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions['schema']]['Tables'] + : never = never +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends { + Update: infer U + } + ? U + : never + : PublicTableNameOrOptions extends keyof PublicSchema['Tables'] + ? PublicSchema['Tables'][PublicTableNameOrOptions] extends { + Update: infer U + } + ? U + : never + : never + +export type Enums< + PublicEnumNameOrOptions extends keyof PublicSchema['Enums'] | { schema: keyof Database }, + EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicEnumNameOrOptions['schema']]['Enums'] + : never = never +> = PublicEnumNameOrOptions extends { schema: keyof Database } + ? Database[PublicEnumNameOrOptions['schema']]['Enums'][EnumName] + : PublicEnumNameOrOptions extends keyof PublicSchema['Enums'] + ? PublicSchema['Enums'][PublicEnumNameOrOptions] + : never + +export type CompositeTypes< + PublicCompositeTypeNameOrOptions extends + | keyof PublicSchema['CompositeTypes'] + | { schema: keyof Database }, + CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'] + : never = never +> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } + ? Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName] + : PublicCompositeTypeNameOrOptions extends keyof PublicSchema['CompositeTypes'] + ? PublicSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions] + : never + +type Custom = { + version: number + events: Array<{ + type: string + [x: string]: any + }> +} + +export type DatabaseOverride = MergeDeep< + Database, + { + public: { + Tables: { + foo: { + Row: { + bar: Custom + baz: Custom + } + Insert: { + bar: Custom + baz: Custom + } + Update: { + bar?: Custom + baz?: Custom + } + } + } + } + } +> + +const postgrest = new PostgrestClient('http://localhost:3000') + +const postgrestOverrideTypes = new PostgrestClient('http://localhost:3000') + +// Basic types +{ + const res = await postgrest.from('foo').select('id').eq('id', '...').single() + + const bar = {} as Custom + const baz = {} as Custom + if (res.error) { + throw new Error(res.error.message) + } + const result = await postgrest + .from('foo') + .update({ + bar, + baz, + }) + .eq('id', res.data.id) + expectType(result.data) +} + +// extended types +{ + const res = await postgrestOverrideTypes + .from('foo') + .select('id, bar, baz') + .eq('id', '...') + .single() + + const bar = {} as Custom + const baz = {} as Custom + if (res.error) { + throw new Error(res.error.message) + } + const result = await postgrestOverrideTypes + .from('foo') + .update({ + bar, + baz, + }) + .eq('id', res.data.id) + expectType(result.data) +} From 8b7f24190fe497520f8ca8d14e5fc04987658dc9 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 20 Jan 2025 18:01:35 +0900 Subject: [PATCH 24/31] wip: error occurs when typescript upgrade --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index a78cf6f5..76c86bb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "tsd": "^0.31.2", "type-fest": "^4.32.0", "typedoc": "^0.22.16", - "typescript": "4.5.5", + "typescript": "^4.7.4", "wait-for-localhost-cli": "^3.0.0" } }, @@ -5993,9 +5993,9 @@ } }, "node_modules/typescript": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", - "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -10629,9 +10629,9 @@ } }, "typescript": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", - "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", "dev": true }, "v8-to-istanbul": { diff --git a/package.json b/package.json index 5b2d4233..11224888 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "tsd": "^0.31.2", "type-fest": "^4.32.0", "typedoc": "^0.22.16", - "typescript": "4.5.5", + "typescript": "^4.7.4", "wait-for-localhost-cli": "^3.0.0" } } From c3aea0daf4be623241d730a2282f168a84c98304 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 20 Jan 2025 18:22:08 +0900 Subject: [PATCH 25/31] chore: use direct replacement instead of MergeDeep --- test/issue-1354-d.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/issue-1354-d.ts b/test/issue-1354-d.ts index b9840a0a..4206e1a4 100644 --- a/test/issue-1354-d.ts +++ b/test/issue-1354-d.ts @@ -10,27 +10,27 @@ export type Database = { foo: { Row: { created_at: string | null - bar: Json + bar: Custom id: string - baz: Json + baz: Custom game_id: string updated_at: string | null user_id: string | null } Insert: { created_at?: string | null - bar: Json + bar: Custom id?: string - baz: Json + baz: Custom game_id: string updated_at?: string | null user_id?: string | null } Update: { created_at?: string | null - bar?: Json + bar?: Custom id?: string - baz?: Json + baz?: Custom game_id?: string updated_at?: string | null user_id?: string | null From 7816a4d3fe4d4dba16a94fb0cb6ee6c87a73483f Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 20 Jan 2025 22:26:21 +0900 Subject: [PATCH 26/31] fix(types): type instantiation is excessively deep and possibly infinite When using MergeDeep type-fest even with simple structures we get this error For typescript > 4.5.5 this trick fix the issue by forcing TS to infer the value And reuse it instead of computing multiple time --- src/PostgrestFilterBuilder.ts | 24 +++++++++++++++--------- test/issue-1354-d.ts | 12 ++++++------ 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/PostgrestFilterBuilder.ts b/src/PostgrestFilterBuilder.ts index 4dbee83c..8b39c8bd 100644 --- a/src/PostgrestFilterBuilder.ts +++ b/src/PostgrestFilterBuilder.ts @@ -73,9 +73,11 @@ export default class PostgrestFilterBuilder< */ eq( column: ColumnName, - value: ResolveFilterValue extends never - ? NonNullable - : NonNullable> + value: ResolveFilterValue extends infer ResolvedFilterValue + ? ResolvedFilterValue extends never + ? NonNullable + : NonNullable + : never ): this { this.url.searchParams.append(column, `eq.${value}`) return this @@ -89,9 +91,11 @@ export default class PostgrestFilterBuilder< */ neq( column: ColumnName, - value: ResolveFilterValue extends never - ? unknown - : ResolveFilterValue + value: ResolveFilterValue extends infer ResolvedFilterValue + ? ResolvedFilterValue extends never + ? unknown + : NonNullable + : never ): this { this.url.searchParams.append(column, `neq.${value}`) return this @@ -269,9 +273,11 @@ export default class PostgrestFilterBuilder< */ in( column: ColumnName, - values: ResolveFilterValue extends never - ? unknown[] - : ReadonlyArray> + values: ResolveFilterValue extends infer ResolvedFilterValue + ? ResolvedFilterValue extends never + ? unknown[] + : ReadonlyArray + : never ): this { const cleanedValues = Array.from(new Set(values)) .map((s) => { diff --git a/test/issue-1354-d.ts b/test/issue-1354-d.ts index 4206e1a4..b9840a0a 100644 --- a/test/issue-1354-d.ts +++ b/test/issue-1354-d.ts @@ -10,27 +10,27 @@ export type Database = { foo: { Row: { created_at: string | null - bar: Custom + bar: Json id: string - baz: Custom + baz: Json game_id: string updated_at: string | null user_id: string | null } Insert: { created_at?: string | null - bar: Custom + bar: Json id?: string - baz: Custom + baz: Json game_id: string updated_at?: string | null user_id?: string | null } Update: { created_at?: string | null - bar?: Custom + bar?: Json id?: string - baz?: Custom + baz?: Json game_id?: string updated_at?: string | null user_id?: string | null From 7ca20e7809d4ebce5c9de8d42e4ca41f9c2f01f5 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 20 Jan 2025 22:27:59 +0900 Subject: [PATCH 27/31] chore: revert package --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 76c86bb4..3fb7db94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "tsd": "^0.31.2", "type-fest": "^4.32.0", "typedoc": "^0.22.16", - "typescript": "^4.7.4", + "typescript": "^4.5.5", "wait-for-localhost-cli": "^3.0.0" } }, diff --git a/package.json b/package.json index 11224888..d46e8b73 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "tsd": "^0.31.2", "type-fest": "^4.32.0", "typedoc": "^0.22.16", - "typescript": "^4.7.4", + "typescript": "^4.5.5", "wait-for-localhost-cli": "^3.0.0" } } From 13a65ec7d386025ae222b07223ab4f72a145c815 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 20 Jan 2025 22:48:06 +0900 Subject: [PATCH 28/31] fix: in filter result infer --- src/PostgrestFilterBuilder.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/PostgrestFilterBuilder.ts b/src/PostgrestFilterBuilder.ts index 8b39c8bd..489b7637 100644 --- a/src/PostgrestFilterBuilder.ts +++ b/src/PostgrestFilterBuilder.ts @@ -273,11 +273,13 @@ export default class PostgrestFilterBuilder< */ in( column: ColumnName, - values: ResolveFilterValue extends infer ResolvedFilterValue - ? ResolvedFilterValue extends never - ? unknown[] - : ReadonlyArray - : never + values: ReadonlyArray< + ResolveFilterValue extends infer ResolvedFilterValue + ? ResolvedFilterValue extends never + ? unknown[] + : ResolvedFilterValue + : never + > ): this { const cleanedValues = Array.from(new Set(values)) .map((s) => { From e7089f620c685bfa62090f605fbce9e1b80e850c Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 20 Jan 2025 23:24:10 +0900 Subject: [PATCH 29/31] fix: in filter --- src/PostgrestFilterBuilder.ts | 6 +++--- test/issue-1354-d.ts | 13 +++++++++++++ test/relationships.ts | 7 ++++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/PostgrestFilterBuilder.ts b/src/PostgrestFilterBuilder.ts index 489b7637..4b47beb8 100644 --- a/src/PostgrestFilterBuilder.ts +++ b/src/PostgrestFilterBuilder.ts @@ -75,9 +75,9 @@ export default class PostgrestFilterBuilder< column: ColumnName, value: ResolveFilterValue extends infer ResolvedFilterValue ? ResolvedFilterValue extends never - ? NonNullable + ? unknown : NonNullable - : never + : unknown ): this { this.url.searchParams.append(column, `eq.${value}`) return this @@ -95,7 +95,7 @@ export default class PostgrestFilterBuilder< ? ResolvedFilterValue extends never ? unknown : NonNullable - : never + : unknown ): this { this.url.searchParams.append(column, `neq.${value}`) return this diff --git a/test/issue-1354-d.ts b/test/issue-1354-d.ts index b9840a0a..e388bf30 100644 --- a/test/issue-1354-d.ts +++ b/test/issue-1354-d.ts @@ -212,4 +212,17 @@ const postgrestOverrideTypes = new PostgrestClient('http://loc }) .eq('id', res.data.id) expectType(result.data) + const resIn = await postgrestOverrideTypes + .from('foo') + .select('id, bar, baz') + .in('bar', [ + { version: 1, events: [] }, + { version: 2, events: [] }, + ]) + .single() + + if (resIn.error) { + throw new Error(resIn.error.message) + } + expectType<{ id: string; bar: Custom; baz: Custom }>(resIn.data) } diff --git a/test/relationships.ts b/test/relationships.ts index faca706a..7c4110aa 100644 --- a/test/relationships.ts +++ b/test/relationships.ts @@ -1830,7 +1830,12 @@ test('self reference relation via column', async () => { }) test('aggregate on missing column with alias', async () => { - const res = await selectQueries.aggregateOnMissingColumnWithAlias.eq('id', 1).limit(1).single() + const res = await selectQueries.aggregateOnMissingColumnWithAlias + // @ts-expect-error should not be able to eq 'id' since the column does not + // exist + .eq('id', 2) + .limit(1) + .single() expect(res).toMatchInlineSnapshot(` Object { "count": null, From b49c66098b94ccec2be4e3e598aaa4eac2a19151 Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 23 Jan 2025 14:41:23 +0900 Subject: [PATCH 30/31] fix: jsonpath accessor and filters --- src/PostgrestFilterBuilder.ts | 43 +++++++++++++++--------- test/issue-1354-d.ts | 62 +++++++++++++++++++++++++++++++++++ test/relationships.ts | 7 +--- 3 files changed, 91 insertions(+), 21 deletions(-) diff --git a/src/PostgrestFilterBuilder.ts b/src/PostgrestFilterBuilder.ts index 4b47beb8..8fe560bb 100644 --- a/src/PostgrestFilterBuilder.ts +++ b/src/PostgrestFilterBuilder.ts @@ -1,4 +1,5 @@ import PostgrestTransformBuilder from './PostgrestTransformBuilder' +import { JsonPathToAccessor, JsonPathToType } from './select-query-parser/utils' import { GenericSchema } from './types' type FilterOperator = @@ -40,6 +41,12 @@ type ResolveFilterValue< : ResolveFilterRelationshipValue : ColumnName extends keyof Row ? Row[ColumnName] + : // If the column selection is a jsonpath like `data->value` we attempt to match + // the expected type with the parsed custom json type + JsonPathToType> extends infer JsonPathValue + ? JsonPathValue extends never + ? never + : JsonPathValue : never type ResolveFilterRelationshipValue< @@ -73,11 +80,14 @@ export default class PostgrestFilterBuilder< */ eq( column: ColumnName, - value: ResolveFilterValue extends infer ResolvedFilterValue - ? ResolvedFilterValue extends never - ? unknown - : NonNullable - : unknown + value: ResolveFilterValue extends never + ? NonNullable + : // We want to infer the type before wrapping it into a `NonNullable` to avoid too deep + // type resolution error + ResolveFilterValue extends infer ResolvedFilterValue + ? NonNullable + : // We should never enter this case as all the branches are covered above + never ): this { this.url.searchParams.append(column, `eq.${value}`) return this @@ -91,11 +101,11 @@ export default class PostgrestFilterBuilder< */ neq( column: ColumnName, - value: ResolveFilterValue extends infer ResolvedFilterValue - ? ResolvedFilterValue extends never - ? unknown - : NonNullable - : unknown + value: ResolveFilterValue extends never + ? unknown + : ResolveFilterValue extends infer ResolvedFilterValue + ? ResolvedFilterValue + : never ): this { this.url.searchParams.append(column, `neq.${value}`) return this @@ -274,11 +284,14 @@ export default class PostgrestFilterBuilder< in( column: ColumnName, values: ReadonlyArray< - ResolveFilterValue extends infer ResolvedFilterValue - ? ResolvedFilterValue extends never - ? unknown[] - : ResolvedFilterValue - : never + ResolveFilterValue extends never + ? unknown + : // We want to infer the type before wrapping it into a `NonNullable` to avoid too deep + // type resolution error + ResolveFilterValue extends infer ResolvedFilterValue + ? ResolvedFilterValue + : // We should never enter this case as all the branches are covered above + never > ): this { const cleanedValues = Array.from(new Set(values)) diff --git a/test/issue-1354-d.ts b/test/issue-1354-d.ts index e388bf30..704f0ae7 100644 --- a/test/issue-1354-d.ts +++ b/test/issue-1354-d.ts @@ -191,6 +191,35 @@ const postgrestOverrideTypes = new PostgrestClient('http://loc expectType(result.data) } +// basic types with postgres jsonpath selector +{ + const res = await postgrest.from('foo').select('id, bar, baz').eq('bar->version', 31).single() + + const bar = {} as Json + const baz = {} as Json + if (res.error) { + throw new Error(res.error.message) + } + const result = await postgrest + .from('foo') + .update({ + bar, + baz, + }) + .eq('bar->version', 31) + expectType(result.data) + const resIn = await postgrest + .from('foo') + .select('id, bar, baz') + .in('bar->version', [1, 2]) + .single() + + if (resIn.error) { + throw new Error(resIn.error.message) + } + expectType<{ id: string; bar: Json; baz: Json }>(resIn.data) +} + // extended types { const res = await postgrestOverrideTypes @@ -226,3 +255,36 @@ const postgrestOverrideTypes = new PostgrestClient('http://loc } expectType<{ id: string; bar: Custom; baz: Custom }>(resIn.data) } + +// extended types with postgres jsonpath selector +{ + const res = await postgrestOverrideTypes + .from('foo') + .select('id, bar, baz') + .eq('bar->version', 31) + .single() + + const bar = {} as Custom + const baz = {} as Custom + if (res.error) { + throw new Error(res.error.message) + } + const result = await postgrestOverrideTypes + .from('foo') + .update({ + bar, + baz, + }) + .eq('bar->version', res.data.bar.version) + expectType(result.data) + const resIn = await postgrestOverrideTypes + .from('foo') + .select('id, bar, baz') + .in('bar->version', [1, 32]) + .single() + + if (resIn.error) { + throw new Error(resIn.error.message) + } + expectType<{ id: string; bar: Custom; baz: Custom }>(resIn.data) +} diff --git a/test/relationships.ts b/test/relationships.ts index 7c4110aa..bf40a1f1 100644 --- a/test/relationships.ts +++ b/test/relationships.ts @@ -1830,12 +1830,7 @@ test('self reference relation via column', async () => { }) test('aggregate on missing column with alias', async () => { - const res = await selectQueries.aggregateOnMissingColumnWithAlias - // @ts-expect-error should not be able to eq 'id' since the column does not - // exist - .eq('id', 2) - .limit(1) - .single() + const res = await selectQueries.aggregateOnMissingColumnWithAlias.eq('id', 2).limit(1).single() expect(res).toMatchInlineSnapshot(` Object { "count": null, From c2112d4a1e618746ccd140bd110509eb127a727e Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 23 Jan 2025 14:58:29 +0900 Subject: [PATCH 31/31] fix: jsonpath filter string operator accessor --- src/PostgrestFilterBuilder.ts | 10 ++++++++-- test/issue-1354-d.ts | 8 +++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/PostgrestFilterBuilder.ts b/src/PostgrestFilterBuilder.ts index 8fe560bb..c0de7d33 100644 --- a/src/PostgrestFilterBuilder.ts +++ b/src/PostgrestFilterBuilder.ts @@ -26,6 +26,10 @@ type FilterOperator = | 'phfts' | 'wfts' +export type IsStringOperator = Path extends `${string}->>${string}` + ? true + : false + // Match relationship filters with `table.column` syntax and resolve underlying // column value. If not matched, fallback to generic type. // TODO: Validate the relationship itself ala select-query-parser. Currently we @@ -41,9 +45,11 @@ type ResolveFilterValue< : ResolveFilterRelationshipValue : ColumnName extends keyof Row ? Row[ColumnName] - : // If the column selection is a jsonpath like `data->value` we attempt to match + : // If the column selection is a jsonpath like `data->value` or `data->>value` we attempt to match // the expected type with the parsed custom json type - JsonPathToType> extends infer JsonPathValue + IsStringOperator extends true + ? string + : JsonPathToType> extends infer JsonPathValue ? JsonPathValue extends never ? never : JsonPathValue diff --git a/test/issue-1354-d.ts b/test/issue-1354-d.ts index 704f0ae7..247efa43 100644 --- a/test/issue-1354-d.ts +++ b/test/issue-1354-d.ts @@ -280,7 +280,13 @@ const postgrestOverrideTypes = new PostgrestClient('http://loc const resIn = await postgrestOverrideTypes .from('foo') .select('id, bar, baz') - .in('bar->version', [1, 32]) + .in('bar->version', [31]) + .single() + await postgrestOverrideTypes + .from('foo') + .select('id, bar, baz') + // the type become a string when using the string json accessor operator + .in('bar->>version', ['something']) .single() if (resIn.error) {