Skip to content
This repository was archived by the owner on Jun 19, 2025. It is now read-only.

Comments

chore: move to biome#60

Open
samchouse wants to merge 1 commit into06-17-feat_app_add_strict_permissions_modefrom
07-16-chore_move_to_eslint_v9
Open

chore: move to biome#60
samchouse wants to merge 1 commit into06-17-feat_app_add_strict_permissions_modefrom
07-16-chore_move_to_eslint_v9

Conversation

@samchouse
Copy link
Owner

@samchouse samchouse commented Jul 17, 2024

In case this ever becomes useful in the future.

// packages/lib/src/modules/tables/index.ts
/* eslint-disable @stylistic/max-len */
import { Row, TFWithModifiers, TInfer, Table, table } from './fields';
import { Field, FieldCrud } from '../../types';
import { merge } from '../../util';
import { BaseModule } from '../util';

export { Row, TFWithModifiers, TField, TInfer, Table, table } from './fields';

export interface CustomTable {
  id: string;
  name: string;
  fields: Field[];
  createdAt: string;
  updatedAt: string;
  permissions: {
    view: string | null;
    create: string | null;
    update: string | null;
    delete: string | null;
  };
}

// function hjg<T extends { one: boolean }>(options: T): [T['one']] extends [true] ? true : false {
//   return true;
// }

// const ab = hjg({ one: false });

export class TablesModule extends BaseModule {
  public async list() {
    return this.client.json<CustomTable[]>({
      path: '/tables/list',
      method: 'GET',
      projectIdNeeded: true,
    });
  }

  public async create<T>(
    table: T extends Table
      ? T
      : ReturnType<
        Table['requestBody']
      >,
  ) {
    return this.client.json<CustomTable>({
      path: '/tables/create',
      method: 'POST',
      body: JSON.stringify(
        table instanceof Table ? table.requestBody() : table,
      ),
      projectIdNeeded: true,
    });
  }

  public async update<T>(
    name: T extends Table<infer U> ? U : string,
    body: {
      name?: string;
      fields?: FieldCrud[];
      permissions?: {
        view: string | null;
        create: string | null;
        update: string | null;
        delete: string | null;
      };
    },
  ) {
    return this.client.json<CustomTable>({
      path: `/tables/update/${name}`,
      method: 'PATCH',
      body: JSON.stringify(body),
      projectIdNeeded: true,
    });
  }

  public async delete<T>(name: T extends Table<infer U> ? U : string) {
    return this.client.json({
      path: `/tables/delete/${name}`,
      method: 'DELETE',
      projectIdNeeded: true,
    });
  }

  public async get<T extends Row | Table = Row, U = T extends Table ? TInfer<T> : T, V extends {
    name: T extends Table<infer U> ? U : string;
    options: {
      one: true;
      match?: Partial<U>;
    }
    | {
      one: false;
      page?: number;
      limit?: number;
      match?: Partial<U>;
    };
  } = {
    name: T extends Table<infer U> ? U : string;
    options: {
      one: true;
      match?: Partial<U>;
    }
    | {
      one: false;
      page?: number;
      limit?: number;
      match?: Partial<U>;
    };
  }>(
    { name, options = { one: true } }: V,
  ) {
    return this.client.json<
      [V['options']['one']] extends [true]
        ? U
        : {
            rows: U[];
            pagination: {
              records: number;
              pages: number;
            };
          }
    >({
      path: merge(
        options.one ? `/tables/${name}/row` : `/tables/${name}/rows`,
        options.match
        && `?${Object.entries<
            string | number | boolean | string[] | Date | undefined
        >(options.match)
          .map(([k, v]) => `${k}=${v?.toString() ?? ''}`)
          .join('&')}`,
      ),
      method: 'GET',
      projectIdNeeded: true,
    });
  }

  public async createRow<T>(
    table: T extends Table<infer _, infer U> ? U : string,
    data: T extends Table<infer _>
      ? TInfer<T>
      : Omit<UnknownData, keyof BaseData> & Partial<BaseData>,
  ) {
    return this.client.json<Required<typeof data>>({
      path: `/tables/${table}/create`,
      method: 'POST',
      body: JSON.stringify(data),
      projectIdNeeded: true,
    });
  }

  public async updateRow<T>(
    table: T extends Table<infer _, infer U> ? U : string,
    match: T extends Table<infer _>
      ? Partial<TInfer<T>>
      : Partial<BaseData>,
    data: T extends Table<infer _>
      ? Partial<TInfer<T>>
      : Partial<UnknownData>,
  ) {
    return this.client.json<Required<typeof data>>({
      path: merge(
        `/tables/${table}/update?`,
        Object.entries<string | number | boolean | string[] | Date | undefined>(
          match,
        )
          .map(([k, v]) => `${k}=${v?.toString() ?? ''}`)
          .join('&'),
      ),
      method: 'PATCH',
      body: JSON.stringify(data),
      projectIdNeeded: true,
    });
  }

  public async deleteRow<T>(
    table: T extends Table<infer _, infer U> ? U : string,
    match: T extends Table<infer _>
      ? Partial<TInfer<T>>
      : Partial<BaseData>,
  ) {
    return this.client.json({
      path: merge(
        `/tables/${table}/delete?`,
        Object.entries<string | number | boolean | string[] | Date | undefined>(
          match,
        )
          .map(([k, v]) => `${k}=${v?.toString() ?? ''}`)
          .join('&'),
      ),
      method: 'DELETE',
      projectIdNeeded: true,
    });
  }
}

const a = new TablesModule();

const t = table('asd', (b) => ({
  sd: b.string(),
}));

void a.get<typeof t>({
  name: 'asd',
}).then((d) => { console.log(d.rows[0]); });

void a.get<typeof t>({
  name: 'asd',
  options: {
    one: false,
    match: {},
  },
}).then((d) => { console.log(d.rows[0]); });

void a.get<typeof t>({
  name: 'asd',
  options: {
    one: true,
    match: {
      sd: 'asd',
      id: 'asd',
    },
  },
}).then((d) => { console.log(d.rows[0]); });

void a.get<Row<{ asd: 'asd' }>>({
  name: 'asd',
  options: {
    one: true,
    match: {
      id: 'asd',
      sdd: 'asd',
    },
  },
}).then((d) => { console.log(d.rows[0]); });

void a.get({
  name: 'asd',
  options: {
    one: true,
    match: {
      id: 'asd',
      asd: 'asd',
    },
  },
}).then((d) => { console.log(d); });
// packages/lib/src/modules/tables/fields/index.ts
import { z } from 'zod';

import {
  TFBoolean,
  TFDate,
  TFEmail,
  TFNumber,
  TFRelationMany,
  TFRelationSingle,
  TFSelect,
  TFString,
  TFUrl,
} from './fields';
import { TFOptional, TFUnique } from './shared';
import { Field } from '../../../types';

type Data = Record<string, string | number | boolean | string[] | Date | undefined>;
export type Row<T extends Data = Data> = T & {
  id: string;
  createdAt: Date;
  updatedAt: Date;
};

type ZExtend<T> =
  T extends TFOptional<infer U>
    ? z.ZodOptional<ZExtend<U>>
    : T extends TFUnique<infer U>
      ? ZExtend<U>
      : T extends TFString
        ? z.ZodString
        : T extends TFNumber
          ? z.ZodNumber
          : T extends TFBoolean
            ? z.ZodBoolean
            : T extends TFDate
              ? z.ZodDate
              : T extends TFEmail
                ? z.ZodString
                : T extends TFUrl
                  ? z.ZodString
                  : T extends TFSelect
                    ? z.ZodArray<z.ZodString>
                    : T extends TFRelationSingle
                      ? z.ZodString
                      : T extends TFRelationMany
                        ? z.ZodArray<z.ZodString>
                        : never;

export type TField =
  | TFString
  | TFNumber
  | TFBoolean
  | TFDate
  | TFEmail
  | TFUrl
  | TFSelect
  | TFRelationSingle
  | TFRelationMany;

type TFOptionalWrapper<
  T extends
  | Exclude<TField, TFBoolean>
  | TFUniqueWrapper<Exclude<TField, TFBoolean>>,
> = T extends unknown ? TFOptional<T> : never;

type TFUniqueWrapper<
  T extends
  | Exclude<TField, TFBoolean>
  | TFOptionalWrapper<Exclude<TField, TFBoolean>>,
> = T extends unknown ? TFUnique<T> : never;

export type TFWithModifiers =
  | TField
  | TFOptionalWrapper<
      Exclude<TField, TFBoolean> | TFUniqueWrapper<Exclude<TField, TFBoolean>>
  >
  | TFUniqueWrapper<
      Exclude<TField, TFBoolean> | TFOptionalWrapper<Exclude<TField, TFBoolean>>
  >;

export type TInfer<T> =
  T extends Table
    ? Row<z.infer<ReturnType<T['schema']>>>
    : never;

export class Table<
  T extends string = string,
  U extends Record<string, TFWithModifiers> = Record<string, TFWithModifiers>,
> {
  constructor(
    public name: T,
    private shape: U,
  ) {}

  requestBody() {
    return Object.keys(this.shape).reduce(
      (acc, key) => {
        const field = this.shape[key];
        switch (field.type) {
          case 'string':
            acc.fields.push({
              name: key,
              type: 'string',
              minLength: field.values.minLength,
              maxLength: field.values.maxLength,
              pattern: field.values.pattern?.toString(),
              isUnique: field.modifiers.includes('unique'),
              isRequired: !field.modifiers.includes('optional'),
            });
            break;
          case 'number':
            acc.fields.push({
              name: key,
              type: 'number',
              min: field.values.min,
              max: field.values.max,
              isUnique: field.modifiers.includes('unique'),
              isRequired: !field.modifiers.includes('optional'),
            });
            break;
          case 'boolean':
            acc.fields.push({
              name: key,
              type: 'boolean',
            });
            break;
          case 'date':
            acc.fields.push({
              name: key,
              type: 'date',
              isUnique: field.modifiers.includes('unique'),
              isRequired: !field.modifiers.includes('optional'),
            });
            break;
          case 'email':
            acc.fields.push({
              name: key,
              type: 'email',
              only: field.values.only,
              except: field.values.except,
              isUnique: field.modifiers.includes('unique'),
              isRequired: !field.modifiers.includes('optional'),
            });
            break;
          case 'url':
            acc.fields.push({
              name: key,
              type: 'url',
              only: field.values.only,
              except: field.values.except,
              isUnique: field.modifiers.includes('unique'),
              isRequired: !field.modifiers.includes('optional'),
            });
            break;
          case 'select':
            acc.fields.push({
              name: key,
              type: 'select',
              options: field.values.options,
              minSelected: field.values.minSelected,
              maxSelected: field.values.maxSelected,
              isUnique: field.modifiers.includes('unique'),
              isRequired: !field.modifiers.includes('optional'),
            });
            break;
          case 'relationSingle':
            acc.fields.push({
              name: key,
              type: 'relation',
              target: 'single',
              table: field.values.table,
              cascadeDelete: field.values.cascadeDelete,
              isUnique: field.modifiers.includes('unique'),
              isRequired: !field.modifiers.includes('optional'),
            });
            break;
          case 'relationMany':
            acc.fields.push({
              name: key,
              target: 'many',
              type: 'relation',
              table: field.values.table,
              maxSelected: field.values.maxSelected,
              minSelected: field.values.minSelected,
              cascadeDelete: field.values.cascadeDelete,
              isUnique: field.modifiers.includes('unique'),
              isRequired: !field.modifiers.includes('optional'),
            });
            break;
        }

        return acc;
      },
      {
        name: this.name,
        fields: [] as Field[],
        permissions: {
          view: null,
          create: null,
          update: null,
          delete: null,
        } as {
          view: string | null;
          create: string | null;
          update: string | null;
          delete: string | null;
        },
      },
    );
  }

  schema(): z.ZodObject<{
    [Key in keyof U]: ZExtend<U[Key]>;
  }> {
    return z.object(
      Object.keys(this.shape).reduce<Record<string, z.ZodTypeAny>>(
        (acc, key) => {
          const field = this.shape[key];
          switch (field.type) {
            case 'string':
            case 'relationSingle':
              acc[key] = z.string();
              break;
            case 'number':
              acc[key] = z.number();
              break;
            case 'boolean':
              acc[key] = z.boolean();
              break;
            case 'date':
              acc[key] = z.date();
              break;
            case 'email':
              acc[key] = z.string().email();
              break;
            case 'url':
              acc[key] = z.string().url();
              break;
            case 'select':
            case 'relationMany':
              acc[key] = z.array(z.string());
              break;
          }

          if (field.type !== 'boolean' && field.modifiers.includes('optional'))
            acc[key] = acc[key].optional();

          return acc;
        },
        {},
      ),
    ) as z.ZodObject<{
      [Key in keyof U]: ZExtend<U[Key]>;
    }>;
  }
}

class TBuilder {
  string() {
    return new TFString();
  }

  number() {
    return new TFNumber();
  }

  boolean() {
    return new TFBoolean();
  }

  date() {
    return new TFDate();
  }

  email() {
    return new TFEmail();
  }

  url() {
    return new TFUrl();
  }

  select({ options }: { options: string[] }) {
    return new TFSelect({ options });
  }

  relation<T extends { target: 'single' | 'many' }>({
    table,
    target,
  }: {
    table: string;
  } & T) {
    return (
      target === 'single'
        ? new TFRelationSingle({
          table,
          cascadeDelete: false,
        })
        : new TFRelationMany({
          table,
          cascadeDelete: false,
        })
    ) as T['target'] extends 'single' ? TFRelationSingle : TFRelationMany;
  }
}

export function table<
  T extends string,
  U extends Record<string, TFWithModifiers>,
>(name: T, shape: (builder: TBuilder) => U): Table<T, U> {
  return new Table(name, shape(new TBuilder()));
}

@github-actions
Copy link

Backend Staging

  • Deploy this PR

Copy link
Owner Author

samchouse commented Jul 17, 2024

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@samchouse samchouse force-pushed the 07-16-chore_move_to_eslint_v9 branch 2 times, most recently from d758943 to 1119640 Compare July 18, 2024 04:15
@samchouse samchouse force-pushed the 07-16-chore_move_to_eslint_v9 branch from 1119640 to da05a70 Compare September 16, 2024 18:54
@samchouse samchouse changed the title chore: move to eslint v9 chore: move to biome Sep 16, 2024
@samchouse samchouse force-pushed the 07-16-chore_move_to_eslint_v9 branch from da05a70 to ec0ff61 Compare September 16, 2024 18:59
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant