Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backwards Compatibility to allow migrating to Runtime-Validation (Zod) #437

Closed
ericmbarnard opened this issue Nov 29, 2022 · 3 comments
Closed

Comments

@ericmbarnard
Copy link

Give long-time slonik users a way to migrate to the value of runtime validation without having to stay pinned on an older version of Slonik. I have working code in production that I'd be glad to share in a PR.

Desired Behavior

Allow non-Zod types for queries (like we've historically had)

interface DataTest { foo: string, bar: string; };
const qry = sql.legacy<DataTest>`SELECT * FROM data_tests WHERE ${whereClause}`;

Allow non-Zod types for query helper functions (like we've historically had):

const results = await db.many<DataTest>(sql.unsafe`SELECT * FROM data_tests WHERE id = ${inserted.id}`);
  • I went with this approach bc it still leaves "marker" names in place to help us find where to go back and fix up properly (the things to search for being sql.legacy and sql.unsafe)
  • This did require some mass-renaming in the codebase ( sql' => sql.unsafe' and sql<' => sql.legacy' )

Motivation

It appears that the breaking changes in v33 and beyond are mostly Typescript related. I put together some of the types below to give us backward compatibility in our codebase as migrating our entire data access layer to Zod models was not something we could do in one shot. We are migrating, as runtime validation is a huge need, but its just not an overnight thing.

Implementation

I am currently using the following types in a ./db/index.ts file that all of my consuming code imports from. I re-export the slonik types from here (overridden) and then make sure none of the consuming code is importing slonik directly.

import {
  createPool,
  DatabasePool as SlonikDatabasePool,
  DatabaseTransactionConnection as SlonikDatabaseTransactionConnection,
  PrimitiveValueExpression,
  QueryResult,
  QuerySqlToken,
  sql as slonikSql,
  TypeParser,
  ValueExpression,
} from 'slonik';

type LegacyCommonQueryMethods = {
  query: <T>(sql: QuerySqlToken, values?: PrimitiveValueExpression[]) => Promise<QueryResult<T>>;
  one: <T>(sql: QuerySqlToken, values?: PrimitiveValueExpression[]) => Promise<T>;
  oneFirst: <T>(sql: QuerySqlToken, values?: PrimitiveValueExpression[]) => Promise<T[keyof T]>;
  many: <T>(sql: QuerySqlToken, values?: PrimitiveValueExpression[]) => Promise<ReadonlyArray<T>>;
  manyFirst: <T>(sql: QuerySqlToken, values?: PrimitiveValueExpression[]) => Promise<ReadonlyArray<T[keyof T]>>;
  maybeOne: <T>(sql: QuerySqlToken, values?: PrimitiveValueExpression[]) => Promise<T | null>;
  maybeOneFirst: <T>(sql: QuerySqlToken, values?: PrimitiveValueExpression[]) => Promise<T[keyof T] | null>;
  exists: (sql: QuerySqlToken, values?: PrimitiveValueExpression[]) => Promise<boolean>;
  any: <T>(sql: QuerySqlToken, values?: PrimitiveValueExpression[]) => Promise<ReadonlyArray<T>>;
};

// TODO: legacy compatibility. Remove this once we're fully uzing Zod runtime validation
type DatabaseTransactionConnection = SlonikDatabaseTransactionConnection & LegacyCommonQueryMethods;
type DatabasePool = SlonikDatabasePool &
  LegacyCommonQueryMethods & {
    transaction: <T>(
      handler: (connection: DatabaseTransactionConnection) => Promise<T>,
      transactionRetryLimit?: number
    ) => Promise<T>;
  };

// slonik: add backwards compatibility for old code
type LegacySql = typeof slonikSql & {
  legacy: <T>(
    parts: readonly string[],
    ...args: readonly ValueExpression[]
  ) => Readonly<{
    parser: z.ZodAny;
    legacyType: T; // <--- access to non-zod type
    type: 'SLONIK_TOKEN_QUERY';
    sql: string;
    values: PrimitiveValueExpression[];
  }>;
};

const sql: LegacySql = {
  ...slonikSql,
  legacy: <T>(parts: readonly string[], ...args: readonly ValueExpression[]) => {
    const query = sql.unsafe(parts, ...args);
    return query as Readonly<{
      parser: z.ZodAny;
      legacyType: T;
      type: 'SLONIK_TOKEN_QUERY';
      sql: string;
      values: PrimitiveValueExpression[];
    }>;
  },
};

// make the imports tidier
export { DatabasePool, sql, DatabaseTransactionConnection };

I also have unit tests that I'd be willing to contribute.

@nathanchapman
Copy link
Sponsor

Agreed, I'm running into this now as well. Being able to gradually migrate from generics to Zod validation would make upgrading significantly easier and allow us to have smaller, testable changesets

@markcellus
Copy link

Agreed! Hmm... so is the zod stuff what's preventing my suggestion in #438?

@gajus
Copy link
Owner

gajus commented Mar 3, 2023

Sorry, but this issue did not get enough attention to make it worthwhile to add backwards compatibility. A mass-replace of

sql`

to

sql.unsafe`

is what is enough for most users.

@gajus gajus closed this as completed Mar 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants