👻

supabase + GraphQL の 検索で in を使えるようにする

2022/04/26に公開
  • 前回

https://zenn.dev/sora_kumo/articles/718f9af36891c6

supabase はそのままだと GraphQL の filter で in が使えない

RestAPI には存在しているのですが、GraphQL には in 演算子が存在していません。そのままだと無理です。

機能を追加する

無いものは仕方がないので作ります。前回の like に追加する形で実装します。
以下を実行すると、in と like が使えるようになります。

create or replace function graphql.text_to_comparison_op(text)
    returns graphql.comparison_op
    language sql
    immutable
    as
$$
    select
        case $1
            when 'eq' then '='
            when 'lt' then '<'
            when 'lte' then '<='
            when 'neq' then '<>'
            when 'gte' then '>='
            when 'gt' then '>'
            when 'like' then 'like'
            when 'in' then 'in'
            else graphql.exception('Invalid comaprison operator')
        end::graphql.comparison_op
$$;


insert into graphql._field(parent_type_id, type_id, constant_name, is_not_null, is_array, description)
    select
        gt.id as parent_type_id,
        gt.graphql_type_id type_id,
        ops.constant_name as constant_name,
        false,
        false,
        null::text as description
    from
        graphql.type gt -- IntFilter
        join (
            values
                ('like')
        ) ops(constant_name)
            on true
    where
        gt.meta_kind = 'FilterType'
        and gt.graphql_type_id = graphql.type_id('String');

insert into graphql._field(parent_type_id, type_id, constant_name, is_not_null, is_array, description)
    select
        gt.id as parent_type_id,
        gt.graphql_type_id type_id,
        ops.constant_name as constant_name,
        true,
        true,
        null::text as description
    from
        graphql.type gt -- IntFilter
        join (
            values
                ('in')
        ) ops(constant_name)
            on true
    where
        gt.meta_kind = 'FilterType';

alter type graphql.comparison_op add value 'like';
alter type graphql.comparison_op add value 'in';

create or replace function graphql.where_clause(
    filter_arg jsonb,
    entity regclass,
    alias_name text,
    variables jsonb default '{}',
    variable_definitions jsonb default '{}'
)
    returns text
    language plpgsql
    immutable
    as
$$
declare
    clause_arr text[] = '{}';
    variable_name text;
    variable_ix int;
    variable_value jsonb;
    variable_part jsonb;

    sel jsonb;
    ix smallint;

    field_name text;
    column_name text;
    column_type regtype;

    field_value_obj jsonb;
    op_name text;
    field_value text;

    format_str text;

    -- Collect allowed comparison columns
    column_fields graphql.field[] = array_agg(f)
        from
            graphql.type t
            left join graphql.field f
                on t.name = f.parent_type
        where
            t.entity = $2
            and t.meta_kind = 'Node'
            and f.column_name is not null;
begin


    -- No filter specified
    if filter_arg is null then
        return 'true';


    elsif (filter_arg -> 'value' ->> 'kind') not in ('ObjectValue', 'Variable') then
        return graphql.exception('Invalid filter argument');

    -- Disallow variable order by clause because it is incompatible with prepared statements
    elsif (filter_arg -> 'value' ->> 'kind') = 'Variable' then
        -- Variable is <Table>Filter
        -- "{"id": {"eq": 1}, ...}"

        variable_name = graphql.name_literal(filter_arg -> 'value');

        variable_ix = graphql.arg_index(
            -- name of argument
            variable_name,
            variable_definitions
        );
        field_value = format('$%s', variable_ix);

        -- "{"id": {"eq": 1}}"
        variable_value = variables -> variable_name;

        if jsonb_typeof(variable_value) <> 'object' then
            return graphql.exception('Invalid filter argument');
        end if;

        for field_name, column_name, column_type, variable_part in
            select
                f.name,
                f.column_name,
                f.column_type,
                je.v -- {"eq": 1}
            from
                jsonb_each(variable_value) je(k, v)
                left join unnest(column_fields) f
                    on je.k = f.name
            loop

            -- Sanity checks
            if column_name is null or jsonb_typeof(variable_part) <> 'object' then
                -- Attempting to filter on field that does not exist
                return graphql.exception('Invalid filter field');
            end if;

            op_name = k from jsonb_object_keys(variable_part) x(k) limit 1;

            clause_arr = clause_arr || format(
                '%I.%I %s (%s::jsonb -> '
                    || format('%L ->> %L', field_name, op_name)
                    || ')::%s',
                alias_name,
                column_name,
                graphql.text_to_comparison_op(op_name),
                field_value,
                column_type
            );

        end loop;



    elsif (filter_arg -> 'value' ->> 'kind') = 'ObjectValue' then

        for sel, ix in
            select
                sel_, ix_
            from
                jsonb_array_elements( filter_arg -> 'value' -> 'fields') with ordinality oba(sel_, ix_)
            loop

            -- Must populate in every loop
            format_str = null;
            field_value = null;
            field_name = graphql.name_literal(sel);

            select
                into column_name, column_type
                f.column_name, f.column_type
            from
                unnest(column_fields) f
            where
                f.name = field_name;

            if column_name is null then
                -- Attempting to filter on field that does not exist
                return graphql.exception('Invalid filter field');
            end if;


            if graphql.is_variable(sel -> 'value') then
                variable_name = (sel -> 'value' -> 'name' ->> 'value');
                variable_ix = graphql.arg_index(
                    -- name of argument
                    variable_name,
                    variable_definitions
                );
                variable_value = variables -> variable_name;


                -- Sanity checks: '{"eq": 3}'
                if jsonb_typeof(variable_value) <> 'object' then
                    return graphql.exception('Invalid filter variable value');

                elsif (select count(1) <> 1 from jsonb_object_keys(variable_value)) then
                    return graphql.exception('Invalid filter variable value');

                end if;

                -- "eq"
                op_name = k from jsonb_object_keys(variable_value) x(k) limit 1;
                field_value = format('$%s', variable_ix);

                select
                    '%I.%I %s (%s::jsonb ->> ' || format('%L', op_name) || ')::%s'
                from
                    jsonb_each(variable_value)
                limit
                    1
                into format_str;

            elsif sel -> 'value' ->> 'kind' <> 'ObjectValue' then
                return graphql.exception('Invalid filter');

            else
                    field_value_obj = sel -> 'value' -> 'fields' -> 0;
                    op_name = graphql.name_literal(field_value_obj);

                    if field_value_obj ->> 'kind' <> 'ObjectField' then
                        return graphql.exception('Invalid filter clause-2');
                    elseif field_value_obj -> 'value' ->> 'kind' = 'ListValue' then
                        format_str = '%I.%I %s %s';
                        field_value_obj = field_value_obj -> 'value' -> 'values';
                        if jsonb_array_length(field_value_obj) = 0 then
                          format_str = '1 = 0';
                        else
                          field_value = (select format('(%s)',string_agg(value::text, ','))
                            from (select jsonb_array_elements(field_value_obj) ->> 'value' as value) as a);
                        end if;
                    elsif (field_value_obj -> 'value' ->> 'kind') = 'Variable' then
                        variable_name = (field_value_obj -> 'value' -> 'name' ->> 'value');
                        if jsonb_typeof(variables -> variable_name) = 'array' then
                          format_str = '%I.%I %s %s';
                          if jsonb_array_length(variables -> variable_name) = 0 then
                            format_str = '1 = 0';
                          else
                            field_value = (select format('(%s)',string_agg(value::text, ','))
                              from (select jsonb_array_elements(variables -> variable_name) as value) as a);
                          end if;
                        else
                          format_str = '%I.%I %s %s::%s';
                          field_value = format(
                              '$%s',
                              graphql.arg_index(
                                  -- name of argument
                                  (field_value_obj -> 'value' -> 'name' ->> 'value'),
                                  variable_definitions
                              )
                          );
                        end if;
                    else
                        format_str = '%I.%I %s %L::%s';
                        field_value = graphql.value_literal(field_value_obj);

                    end if;

            end if;

            clause_arr = clause_arr || format(
                format_str,
                alias_name,
                column_name,
                graphql.text_to_comparison_op(op_name),
                field_value,
                column_type
            );

        end loop;
    end if;

    return array_to_string(clause_arr, ' and ');
end;
$$;

まとめ

in に関しては取り急ぎ作ったので検証が不十分です。
supabase で GraphQL を使っている人は少なそうですが、困っている同士の助けになれば幸いです。

GitHubで編集を提案

Discussion