Comprendre l’Intégration Avancée de TypeScript et Knex pour des Interactions de Base de Données Propres et Efficaces
Dans cet article, nous allons nous plonger dans un module TypeScript qui utilise des techniques avancées pour interagir avec une base de données à l’aide de Knex.js, garantissant une manipulation des données propre et efficace. Nous détaillerons le code, expliquerons les concepts clés et mettrons en évidence les avantages de cette approche.
Notions utilisées
- Définitions de TypeScript:
- Nous utilisons des définitions de types avancées de TypeScript et la bibliothèque
type-festpour nous assurer que nos données respectent des formats spécifiques. Cela améliore la sécurité des types et rend notre code plus prévisible et moins sujet à des erreurs.
- Nous utilisons des définitions de types avancées de TypeScript et la bibliothèque
- Zod pour la validation de schémas:
- Zod est une bibliothèque de déclaration et de validation de schémas orientée TypeScript. Elle permet de définir des schémas pour nos modèles de données et de les valider lors de l’exécution.
- Knex.js pour les interactions avec la base de données:
- Knex.js est un constructeur de requêtes SQL pour Node.js. Il nous permet d’écrire des requêtes SQL de manière programmatique et sécurisée sur le plan des types.
- Fonctions utilitaires de conversion de casse:
- Des fonctions comme
CamelToSnakeCaseetSnakeToCamelCaseaident à convertir les clés des objets entre camelCase et snake_case, garantissant la cohérence entre les objets JavaScript et les enregistrements de la base de données.
- Des fonctions comme
- Type Guards:
- Les Type Guards, comme
isContactDto, nous aident à nous assurer que nos données correspondent aux types attendus avant d’effectuer des opérations, augmentant la sécurité à l’exécution.
- Les Type Guards, comme
Explication du code
import type { Knex } from 'knex';
import { CamelCase, CamelCasedProperties, SnakeCase, SnakeCasedProperties } from 'type-fest';
import { z } from 'zod';
import { knex } from '../db';
//#region Tools
type IterableGenericObject = { [k: string]: any };
function prefixObjectKeys(obj: { [k: string]: any }, prefix: string) {
return Object.fromEntries(
Object.entries(obj)
.map(([k, v]) => [`${prefix}${k}`, v])
);
}
export function CamelToSnakeCase<T extends string>(str: T): SnakeCase<T> {
return str.toString()
.split('')
.map((char) => (char.toUpperCase() === char ? '_' : '') + char.toLowerCase())
.join('') as SnakeCase<T>;
}
export function SnakeToCamelCase<T extends string>(str: T): CamelCase<T> {
return str.toString()
.split('')
.map((char, i, a) => {
if (i < 1) return char.toLowerCase();
if (char === '_') return '';
if (a[i - 1] === '_') return char.toUpperCase();
return char.toLowerCase();
})
.join('') as CamelCase<T>;
}
export function ObjectToSnakeCasedProperties<
I extends IterableGenericObject,
O = SnakeCasedProperties<I>
>(obj: I, keyMap: Map<keyof I, keyof O> = new Map()) {
return Object.fromEntries(
Object.entries(obj)
.map(([prop, value]) => [
keyMap.get(prop) || CamelToSnakeCase(prop),
value
] as const)
) as O;
}
export function ObjectToCamelCasedProperties<
I extends IterableGenericObject,
O = CamelCasedProperties<I>
>(obj: I, keyMap: Map<keyof I, keyof O> = new Map()) {
return Object.fromEntries(
Object.entries(obj)
.map(([prop, value]) => [
keyMap.get(prop) || SnakeToCamelCase(prop),
value
] as const)
) as O;
}
//#endregion
//#region Contact domain
export const ContactSchema = z.object({
id: z.number().positive().gt(0),
firstName: z.string().min(1),
lastName: z.string().min(1),
phoneNumber: z.string().min(8).max(15),
});
export const ContactDtoSchema = ContactSchema.partial();
export const ContactUpsertDtoSchema = ContactSchema.omit({ id: true });
export type Contact = z.infer<typeof ContactSchema>;
export type ContactDto = z.infer<typeof ContactDtoSchema>;
export type ContactUpsertDto = z.infer<typeof ContactUpsertDtoSchema>;
export type ContactTableItem = SnakeCasedProperties<Contact>;
// Type guard example:
export function isContactDto(obj: Record<string, any>): obj is ContactDto {
return ContactSchema.deepPartial().safeParse(obj).success;
}
export function isContactUpsertDto(obj: Record<string, any>): obj is ContactUpsertDto {
return ContactUpsertDtoSchema.safeParse(obj).success;
}
//#endregion
//#region Contact repository
function getKnexInstance(knexInstance = knex) {
return (knexInstance as Knex<ContactTableItem, ContactTableItem[]>)(`contacts`);
}
export const ContactFields = Object.freeze(Object.values(ContactSchema.keyof().Values));
export async function queryContact(contactWhere: ContactDto, fields = ContactFields, txn?: typeof knex): Promise<Contact | null> {
if (!isContactDto(contactWhere)) {
throw new Error('Invalid contact given');
}
const qb = fields.reduce((queryBuilder, col) => {
return queryBuilder.select(`c.${col}`);
}, getKnexInstance(txn));
const where = prefixObjectKeys(ObjectToSnakeCasedProperties<ContactDto>(contactWhere), 'c.');
const results = await qb
.from<ContactTableItem, ContactTableItem[]>({ c: 'contacts' })
.where(where);
const [firstContactRow] = results;
return firstContactRow
? ObjectToCamelCasedProperties<ContactTableItem>(firstContactRow)
: null;
}
export async function insertContact(contact: ContactUpsertDto, txn?: typeof knex): Promise<number> {
if (!isContactUpsertDto(contact)) {
throw new Error('Invalid contact DTO given');
}
const [first] = await getKnexInstance(txn)
.insert<SnakeCasedProperties<ContactUpsertDto>>(ObjectToSnakeCasedProperties<ContactUpsertDto>(contact))
.returning('id') as Pick<ContactTableItem, 'id'>[];
return first.id;
}
export async function updateContact(id: Contact['id'], contact: ContactUpsertDto, txn?: typeof knex): Promise<void> {
if (!isContactUpsertDto(contact)) {
throw new Error('Invalid contact DTO given');
}
await getKnexInstance(txn)
.update(ObjectToSnakeCasedProperties<Omit<ContactDto, 'id'>>(contact))
.where({ id });
}
export async function deleteContact(id: Contact['id'], txn?: typeof knex): Promise<void> {
await getKnexInstance(txn).where({ id }).delete();
}
export async function listContacts(where = {} as ContactDto, fields = ContactFields, txn?: typeof knex): Promise<Contact[]> {
const qb = fields.reduce((queryBuilder, col) => {
return queryBuilder.select(`c.${col}`);
}, getKnexInstance(txn));
const results: ContactTableItem[] = await qb.where(where).from<ContactTableItem>({ c: 'contacts' });
return results.map((record) => ObjectToCamelCasedProperties(record));
}
//#endregion
Avantages de Cette Approche
- Sécurité des types:
- En utilisant TypeScript et Zod, nous garantissons que nos structures de données respectent strictement les types. Cela réduit le risque d’erreurs à l’exécution et améliore la fiabilité du code.
- Conventions de nommage cohérentes:
- Les fonctions utilitaires
CamelToSnakeCaseetSnakeToCamelCaseassurent que les clés des objets sont formatées de manière homogène, facilitant l’interaction entre les objets JavaScript (camelCase) et les bases de données SQL (snake_case).
- Les fonctions utilitaires
- Réutilisabilité et maintenabilité:
- La conception modulaire, avec des fonctions utilitaires et des schémas bien définis, rend la base de code plus facile à entretenir et à étendre. Ajouter de nouvelles fonctionnalités ou modifier celles existantes devient plus simple.
- séparation claire des responsabilités:
- Le code est organisé en sections logiques, comme les outils, les définitions du domaine et les fonctions du dépôt. Cette séparation facilite la compréhension et la gestion du code.
- Validation améliorée:
- Utiliser Zod pour la validation des schémas garantit que les données respectent les formats attendus, permettant de détecter les erreurs tôt dans le processus de développement.
- Requêtes de base de données efficaces:
- Knex.js offre un mécanisme puissant pour construire des requêtes, et notre approche veille à ce que les requêtes soient typées et optimisées pour les performances.
Conclusion
Cette approche exploite les atouts de TypeScript, Zod et Knex.js pour créer une couche de manipulation des données robuste, facile à maintenir et sécurisée sur le plan du typage. En garantissant des conventions de nommage cohérentes et une validation rigoureuse, nous pouvons bâtir des applications fiables qui se développent et se maintiennent plus facilement.




