Drizzle connector with drizzle model features for plain objects (without relations).
This commit is contained in:
		
							parent
							
								
									214cb7f1c1
								
							
						
					
					
						commit
						ff793f31f4
					
				
					 13 changed files with 471 additions and 16 deletions
				
			
		
							
								
								
									
										5
									
								
								.env.test
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.env.test
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | POSTGRES_HOST=localhost | ||||||
|  | POSTGRES_PORT=5432 | ||||||
|  | POSTGRES_USERNAME="sharkitek" | ||||||
|  | POSTGRES_PASSWORD="sharkitek" | ||||||
|  | POSTGRES_DATABASE="sharkitek" | ||||||
|  | @ -1,10 +0,0 @@ | ||||||
| /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ |  | ||||||
| 
 |  | ||||||
| export default { |  | ||||||
|   preset: "ts-jest", |  | ||||||
|   testEnvironment: "node", |  | ||||||
| 
 |  | ||||||
|   roots: [ |  | ||||||
|     "./tests", |  | ||||||
|   ], |  | ||||||
| }; |  | ||||||
							
								
								
									
										14
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										14
									
								
								package.json
									
										
									
									
									
								
							|  | @ -1,6 +1,6 @@ | ||||||
| { | { | ||||||
|   "name": "@sharkitek/drizzle", |   "name": "@sharkitek/drizzle", | ||||||
|   "version": "1.0.0", |   "version": "3.0.0-beta", | ||||||
|   "description": "Drizzle connector for Sharkitek models.", |   "description": "Drizzle connector for Sharkitek models.", | ||||||
|   "repository": "https://code.zeptotech.net/Sharkitek/Drizzle", |   "repository": "https://code.zeptotech.net/Sharkitek/Drizzle", | ||||||
|   "author": { |   "author": { | ||||||
|  | @ -10,7 +10,7 @@ | ||||||
|   "license": "MIT", |   "license": "MIT", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "build": "tsc && vite build", |     "build": "tsc && vite build", | ||||||
|     "test": "jest" |     "test": "vitest" | ||||||
|   }, |   }, | ||||||
|   "type": "module", |   "type": "module", | ||||||
|   "source": "src/index.ts", |   "source": "src/index.ts", | ||||||
|  | @ -23,17 +23,19 @@ | ||||||
|     "access": "public" |     "access": "public" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@types/jest": "^29.5.13", |     "@sharkitek/core": "^3.3.0", | ||||||
|     "@types/node": "^22.7.4", |     "@types/node": "^22.7.4", | ||||||
|  |     "dotenv": "^16.4.5", | ||||||
|     "drizzle-orm": "^0.33.0", |     "drizzle-orm": "^0.33.0", | ||||||
|     "jest": "^29.7.0", |     "postgres": "^3.4.4", | ||||||
|     "ts-jest": "^29.2.5", |  | ||||||
|     "ts-node": "^10.9.2", |     "ts-node": "^10.9.2", | ||||||
|     "typescript": "^5.6.2", |     "typescript": "^5.6.2", | ||||||
|     "vite": "^5.4.8", |     "vite": "^5.4.8", | ||||||
|     "vite-plugin-dts": "^4.2.3" |     "vite-plugin-dts": "^4.2.3", | ||||||
|  |     "vitest": "^2.1.2" | ||||||
|   }, |   }, | ||||||
|   "peerDependencies": { |   "peerDependencies": { | ||||||
|  |     "@sharkitek/core": "^3.0.0", | ||||||
|     "drizzle-orm": "^0.33.0" |     "drizzle-orm": "^0.33.0" | ||||||
|   }, |   }, | ||||||
|   "packageManager": "yarn@4.5.0" |   "packageManager": "yarn@4.5.0" | ||||||
|  |  | ||||||
							
								
								
									
										151
									
								
								src/Drizzle.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								src/Drizzle.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,151 @@ | ||||||
|  | import {IdentifierType, Model, ModelShape, SerializedModel} from "@sharkitek/core"; | ||||||
|  | import {eq, getTableColumns, Table, TableConfig} from "drizzle-orm"; | ||||||
|  | import {PgDatabase} from "drizzle-orm/pg-core"; | ||||||
|  | import {PgQueryResultHKT} from "drizzle-orm/pg-core/session"; | ||||||
|  | import type {ExtractTablesWithRelations, TablesRelationalConfig} from "drizzle-orm/relations"; | ||||||
|  | import {ModelQuery} from "./Query"; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Serialized values converters for database. | ||||||
|  |  */ | ||||||
|  | export const serializedToDatabaseTypes: Record<string, (serializedValue: any) => any> = { | ||||||
|  | 	"date": (serializedValue: string) => new Date(serializedValue), | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Sharkitek model extension for Drizzle. | ||||||
|  |  */ | ||||||
|  | export interface DrizzleExtension< | ||||||
|  | 	ModelType extends Model<Shape, IdentifierType<Shape, Identifier>>, | ||||||
|  | 	TableType extends Table<TC>, TC extends TableConfig, | ||||||
|  | 	TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown>, | ||||||
|  | 	Scopes extends object, | ||||||
|  | 	Shape extends ModelShape, Identifier extends keyof Shape = any, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema> | ||||||
|  | > | ||||||
|  | { | ||||||
|  | 	/** | ||||||
|  | 	 * Drizzle model manager. | ||||||
|  | 	 */ | ||||||
|  | 	drizzle(): DrizzleModel<ModelType, TableType, TC, TQueryResult, TFullSchema, Scopes, Shape, Identifier, TSchema>; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Drizzle model manager. | ||||||
|  |  */ | ||||||
|  | export class DrizzleModel< | ||||||
|  | 	ModelType extends Model<Shape, IdentifierType<Shape, Identifier>>, | ||||||
|  | 	TableType extends Table<TC>, TC extends TableConfig, | ||||||
|  | 	TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown>, | ||||||
|  | 	Scopes extends object, | ||||||
|  | 	Shape extends ModelShape, Identifier extends keyof Shape = any, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema> | ||||||
|  | > | ||||||
|  | { | ||||||
|  | 	constructor(protected model: ModelType, | ||||||
|  | 	            protected database: PgDatabase<TQueryResult, TFullSchema, TSchema>, | ||||||
|  | 	            protected table: TableType, | ||||||
|  | 	            protected scopes: Scopes & ThisType<ModelQuery<ModelType, TableType, TC, TQueryResult, TFullSchema, Shape, Identifier, TSchema>>) | ||||||
|  | 	{} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Get Drizzle database. | ||||||
|  | 	 */ | ||||||
|  | 	getDatabase(): PgDatabase<TQueryResult, TFullSchema, TSchema> | ||||||
|  | 	{ | ||||||
|  | 		return this.database; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Get model Drizzle table. | ||||||
|  | 	 */ | ||||||
|  | 	getTable(): TableType | ||||||
|  | 	{ | ||||||
|  | 		return this.table; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Get scopes. | ||||||
|  | 	 */ | ||||||
|  | 	getScopes(): Scopes & ThisType<ModelQuery<ModelType, TableType, TC, TQueryResult, TFullSchema, Shape, Identifier, TSchema>> | ||||||
|  | 	{ | ||||||
|  | 		return this.scopes; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Save the model in database. | ||||||
|  | 	 */ | ||||||
|  | 	async save(): Promise<boolean> | ||||||
|  | 	{ | ||||||
|  | 		// Get serialized model update.
 | ||||||
|  | 		const serializedModel = this.model.patch(); | ||||||
|  | 
 | ||||||
|  | 		// Get table columns of the model.
 | ||||||
|  | 		const tableColumns = getTableColumns(this.table); | ||||||
|  | 
 | ||||||
|  | 		// Create model data for the database.
 | ||||||
|  | 		const databaseModelData = Object.fromEntries( | ||||||
|  | 			Object.entries(serializedModel).map( | ||||||
|  | 				// Only keep table column values and convert them into database types, if it is required.
 | ||||||
|  | 				([key, value]) => [key, tableColumns?.[key] ? (serializedToDatabaseTypes?.[tableColumns?.[key].dataType]?.(value) ?? value) : undefined] | ||||||
|  | 			) | ||||||
|  | 		) as Partial<SerializedModel<Shape>>; | ||||||
|  | 
 | ||||||
|  | 		// Initialize query result variable.
 | ||||||
|  | 		let result: any; | ||||||
|  | 
 | ||||||
|  | 		if (this.model.isNew()) | ||||||
|  | 		{ // Insert the new model in database and get the inserted row data.
 | ||||||
|  | 			result = (await (this.database.insert(this.table)).values(databaseModelData as any).returning() as any)?.[0]; | ||||||
|  | 		} | ||||||
|  | 		else | ||||||
|  | 		{ // Update model data in database, and get the updated row data.
 | ||||||
|  | 			result = (await (this.database.update(this.table) | ||||||
|  | 				.set(databaseModelData) | ||||||
|  | 				.where(eq((this.table as any)?.[this.model.getIdentifierName()], this.model.getIdentifier()))) | ||||||
|  | 				.returning() as any)?.[0]; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Update model from inserted or updated row data.
 | ||||||
|  | 		this.model.deserialize(result); | ||||||
|  | 
 | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async refresh(): Promise<void> | ||||||
|  | 	{ | ||||||
|  | 		// Update model from up-to-date row data.
 | ||||||
|  | 		this.model.deserialize( | ||||||
|  | 			(( // Retrieve model data from database.
 | ||||||
|  | 				await this.database.select().from(this.table) | ||||||
|  | 					.where(eq((this.table as any)?.[this.model.getIdentifierName()], this.model.getIdentifier())) | ||||||
|  | 			) as any)[0] | ||||||
|  | 		); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Drizzle extension initializer. | ||||||
|  |  * @param database Drizzle database. | ||||||
|  |  * @param table Drizzle database table. | ||||||
|  |  * @param scopes | ||||||
|  |  */ | ||||||
|  | export function drizzle< | ||||||
|  | 	ModelType extends Model<Shape, IdentifierType<Shape, Identifier>>, | ||||||
|  | 	TableType extends Table<TC>, TC extends TableConfig, | ||||||
|  | 	TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown>, | ||||||
|  | 	Scopes extends object, | ||||||
|  | 	Shape extends ModelShape, Identifier extends keyof Shape = any, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>, | ||||||
|  | >( | ||||||
|  | 	database: PgDatabase<TQueryResult, TFullSchema, TSchema>, | ||||||
|  | 	table: TableType, | ||||||
|  | 	scopes: Scopes & ThisType<ModelQuery<ModelType, TableType, TC, TQueryResult, TFullSchema, Shape, Identifier, TSchema>> | ||||||
|  | ): DrizzleExtension<ModelType, TableType, TC, TQueryResult, TFullSchema, Scopes, Shape, Identifier, TSchema> & ThisType<ModelType> | ||||||
|  | { | ||||||
|  | 	// Return initialized drizzle extension.
 | ||||||
|  | 	return { | ||||||
|  | 		drizzle(): DrizzleModel<ModelType, TableType, TC, TQueryResult, TFullSchema, Scopes, Shape, Identifier, TSchema> | ||||||
|  | 		{ | ||||||
|  | 			// Initialize drizzle model manager instance.
 | ||||||
|  | 			return new DrizzleModel<ModelType, TableType, TC, TQueryResult, TFullSchema, Scopes, Shape, Identifier, TSchema>(this, database, table, scopes); | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | } | ||||||
							
								
								
									
										109
									
								
								src/Query.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/Query.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,109 @@ | ||||||
|  | import {AnyModel, IdentifierType, Model, ModelClass, ModelShape, SerializedModel} from "@sharkitek/core"; | ||||||
|  | import {eq, getTableName, Table, TableConfig} from "drizzle-orm"; | ||||||
|  | import {PgDatabase} from "drizzle-orm/pg-core"; | ||||||
|  | import {PgQueryResultHKT} from "drizzle-orm/pg-core/session"; | ||||||
|  | import type {DBQueryConfig, ExtractTablesWithRelations, TablesRelationalConfig} from "drizzle-orm/relations"; | ||||||
|  | import type {KnownKeysOnly} from "drizzle-orm/utils"; | ||||||
|  | import {DrizzleExtension} from "./Drizzle"; | ||||||
|  | import {inArray} from "drizzle-orm/sql/expressions/conditions"; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Model query manager. | ||||||
|  |  */ | ||||||
|  | export class ModelQuery< | ||||||
|  | 	ModelType extends Model<Shape, IdentifierType<Shape, Identifier>>, | ||||||
|  | 	TableType extends Table<TC>, TC extends TableConfig, | ||||||
|  | 	TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown>, | ||||||
|  | 	Shape extends ModelShape, Identifier extends keyof Shape = any, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema> | ||||||
|  | > | ||||||
|  | { | ||||||
|  | 	constructor(protected modelClass: ModelClass<ModelType, Shape, Identifier>, protected modelInstance: ModelType, protected database: PgDatabase<TQueryResult, TFullSchema, TSchema>, protected table: TableType) | ||||||
|  | 	{} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Parse the given model data to a model. | ||||||
|  | 	 * @param rawModel Raw model. | ||||||
|  | 	 * @protected | ||||||
|  | 	 */ | ||||||
|  | 	protected parseModel(rawModel: SerializedModel<Shape>|null): ModelType|null | ||||||
|  | 	{ | ||||||
|  | 		// The raw model is null or undefined, return NULL.
 | ||||||
|  | 		if (!rawModel) return null; | ||||||
|  | 		// Parse the given raw model to a model.
 | ||||||
|  | 		return (new this.modelClass()).deserialize(rawModel) as ModelType; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Find models matching the given configuration. | ||||||
|  | 	 * @param config Request configuration. | ||||||
|  | 	 */ | ||||||
|  | 	async get<TConfig extends DBQueryConfig<"many", true, TSchema, TSchema[keyof TSchema]>>(config?: KnownKeysOnly<TConfig, DBQueryConfig<"many", true, TSchema, TSchema[keyof TSchema]>>): Promise<ModelType[]> | ||||||
|  | 	{ | ||||||
|  | 		// Parse retrieved raw models.
 | ||||||
|  | 		return ( | ||||||
|  | 			// Find many models from the given configuration.
 | ||||||
|  | 			await ((this.database.query as any)?.[getTableName(this.table)]?.findMany(config) as Promise<SerializedModel<Shape>[]>) | ||||||
|  | 		).map(rawModel => this.parseModel(rawModel)).filter((model) => !!model); // Only keep non-null models.
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Find the first model matching the given configuration. | ||||||
|  | 	 * @param config Request configuration. | ||||||
|  | 	 */ | ||||||
|  | 	async first<TSelection extends Omit<DBQueryConfig<"many", true, TSchema, TSchema[keyof TSchema]>, "limit">>(config?: KnownKeysOnly<TSelection, Omit<DBQueryConfig<"many", true, TSchema, TSchema[keyof TSchema]>, "limit">>): Promise<ModelType|null> | ||||||
|  | 	{ | ||||||
|  | 		// Parse retrieved raw model.
 | ||||||
|  | 		return this.parseModel( | ||||||
|  | 			// Find a model from the given configuration.
 | ||||||
|  | 			await ((this.database.query as any)?.[getTableName(this.table)]?.findFirst(config) as Promise<SerializedModel<Shape>>) | ||||||
|  | 		); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Find a model from its identifier. | ||||||
|  | 	 * @param identifier Model identifier. | ||||||
|  | 	 */ | ||||||
|  | 	async find(identifier: IdentifierType<Shape, Identifier>): Promise<ModelType|null> | ||||||
|  | 	{ | ||||||
|  | 		// Find the first model which match the given identifier.
 | ||||||
|  | 		return this.first({ | ||||||
|  | 			where: eq((this.table as any)?.[this.modelInstance.getIdentifierName()], identifier), | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Find many models from their identifier. | ||||||
|  | 	 * @param identifiers Model identifiers. | ||||||
|  | 	 */ | ||||||
|  | 	async findMany(...identifiers: (IdentifierType<Shape, Identifier>|IdentifierType<Shape, Identifier>[])[]): Promise<ModelType[]> | ||||||
|  | 	{ | ||||||
|  | 		// Find many models which match the given identifiers.
 | ||||||
|  | 		return this.get({ | ||||||
|  | 			where: inArray((this.table as any)?.[this.modelInstance.getIdentifierName()], identifiers.flat()), | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Model query builder. | ||||||
|  |  * @param modelClass Model class. | ||||||
|  |  */ | ||||||
|  | export function query< | ||||||
|  | 	ModelType extends Model<Shape, IdentifierType<Shape, Identifier>> & DrizzleExtension<AnyModel, TableType, TC, TQueryResult, TFullSchema, Scopes, any, any, TSchema>, | ||||||
|  | 	TableType extends Table<TC>, TC extends TableConfig, | ||||||
|  | 	TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown>, | ||||||
|  | 	Scopes extends object, | ||||||
|  | 	Shape extends ModelShape, Identifier extends keyof Shape = any, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema> | ||||||
|  | >(modelClass: ModelClass<ModelType, Shape, Identifier>): ModelQuery<ModelType, TableType, TC, TQueryResult, TFullSchema, Shape, Identifier, TSchema> & Scopes | ||||||
|  | { | ||||||
|  | 	// Get model drizzle instance.
 | ||||||
|  | 	const model = new modelClass(); | ||||||
|  | 	const modelDrizzle = model.drizzle(); | ||||||
|  | 	// Create a model query with the model database and table.
 | ||||||
|  | 	return Object.assign( | ||||||
|  | 		new ModelQuery<ModelType, TableType, TC, TQueryResult, TFullSchema, Shape, Identifier, TSchema>( | ||||||
|  | 			modelClass, model, modelDrizzle.getDatabase(), modelDrizzle.getTable(), | ||||||
|  | 		), | ||||||
|  | 		modelDrizzle.getScopes(), | ||||||
|  | 	); | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								src/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/index.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | 
 | ||||||
|  | export * from "./Drizzle"; | ||||||
|  | export * from "./Query"; | ||||||
							
								
								
									
										64
									
								
								tests/Drizzle.test.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								tests/Drizzle.test.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,64 @@ | ||||||
|  | import {test, expect, beforeAll} from "vitest"; | ||||||
|  | import {query} from "../src"; | ||||||
|  | import {Invoice} from "./Invoice"; | ||||||
|  | import {setupDefaultInvoices} from "./database"; | ||||||
|  | 
 | ||||||
|  | beforeAll(async () => { | ||||||
|  | 	await setupDefaultInvoices(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test("retrieve existing models from database", async () => { | ||||||
|  | 	const invoice = await query(Invoice).find(1); | ||||||
|  | 	const invoices = await query(Invoice).findMany(1, 2); | ||||||
|  | 
 | ||||||
|  | 	expect(invoice).not.toBeNull(); | ||||||
|  | 	expect(invoices).toHaveLength(2); | ||||||
|  | 
 | ||||||
|  | 	expect(invoice?.amount).toStrictEqual(5450.12); | ||||||
|  | 
 | ||||||
|  | 	expect(invoices.reduce((total, invoice) => (total + invoice.amount), 0)).toStrictEqual(5450.12 + 1122.54); | ||||||
|  | 
 | ||||||
|  | 	expect(invoices[0].date.getFullYear()).toStrictEqual(1997); | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | test("alter existing model in database", async () => { | ||||||
|  | 	// Get an invoice to change its date.
 | ||||||
|  | 	const invoice = await query(Invoice).find(1) as Invoice; | ||||||
|  | 	invoice.date.setDate(15); | ||||||
|  | 	await invoice.drizzle().save(); | ||||||
|  | 
 | ||||||
|  | 	// Get the same invoice again, to check that the date has been successfully changed.
 | ||||||
|  | 	const updatedInvoice = await query(Invoice).find(1); | ||||||
|  | 	expect(updatedInvoice?.date?.getDate()).toStrictEqual(15); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test("refresh a model from database", async () => { | ||||||
|  | 	// Get an invoice to change its date.
 | ||||||
|  | 	const invoice = await query(Invoice).find(1) as Invoice; | ||||||
|  | 	invoice.date.setDate(30); | ||||||
|  | 	// Refresh from database: it should revert the changes.
 | ||||||
|  | 	await invoice.drizzle().refresh(); | ||||||
|  | 
 | ||||||
|  | 	expect(invoice?.date?.getDate()).toStrictEqual(15); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test("insert a new model in database", async () => { | ||||||
|  | 	const now = new Date(); | ||||||
|  | 
 | ||||||
|  | 	const invoice = new Invoice(); | ||||||
|  | 	invoice.date = now; | ||||||
|  | 	invoice.amount = 12.34; | ||||||
|  | 	invoice.clientName = "client name"; | ||||||
|  | 	invoice.clientAddress = "any address string"; | ||||||
|  | 
 | ||||||
|  | 	await invoice.drizzle().save(); | ||||||
|  | 
 | ||||||
|  | 	expect(invoice.serialize()).toStrictEqual({ | ||||||
|  | 		id: 3, | ||||||
|  | 		date: now.toISOString(), | ||||||
|  | 		amount: "12.34", | ||||||
|  | 		clientName: "client name", | ||||||
|  | 		clientAddress: "any address string", | ||||||
|  | 	}); | ||||||
|  | 	expect(invoice.serialize()).toStrictEqual((await query(Invoice).find(invoice.id))?.serialize()); | ||||||
|  | }); | ||||||
							
								
								
									
										19
									
								
								tests/Invoice.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								tests/Invoice.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | ||||||
|  | import {s} from "@sharkitek/core"; | ||||||
|  | import {drizzle} from "../src"; | ||||||
|  | import {database} from "./database"; | ||||||
|  | import {invoices} from "./schema/invoices"; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Invoice model class example with Sharkitek and its Drizzle connector. | ||||||
|  |  * @see https://code.zeptotech.net/Sharkitek/Core
 | ||||||
|  |  */ | ||||||
|  | export class Invoice extends s.model({ | ||||||
|  | 	id: s.property.numeric(), | ||||||
|  | 	date: s.property.date(), | ||||||
|  | 	amount: s.property.decimal(), | ||||||
|  | 	clientName: s.property.string(), | ||||||
|  | 	clientAddress: s.property.string(), | ||||||
|  | }, "id").extends(drizzle(database, invoices, { | ||||||
|  | })) | ||||||
|  | { | ||||||
|  | } | ||||||
							
								
								
									
										56
									
								
								tests/database.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								tests/database.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,56 @@ | ||||||
|  | import dotenv from "dotenv"; | ||||||
|  | import postgres from "postgres"; | ||||||
|  | import {drizzle} from "drizzle-orm/postgres-js"; | ||||||
|  | import {sql} from "drizzle-orm"; | ||||||
|  | import * as InvoicesSchema from "./schema/invoices"; | ||||||
|  | 
 | ||||||
|  | // Load configuration from environment variables.
 | ||||||
|  | dotenv.config({ | ||||||
|  | 	path: ".env.test", | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const queryClient = postgres(`postgres://${process.env.POSTGRES_USERNAME}:${process.env.POSTGRES_PASSWORD}@${process.env.POSTGRES_HOST}:${process.env.POSTGRES_PORT}/${process.env.POSTGRES_DATABASE}`); | ||||||
|  | export const database = drizzle(queryClient, { | ||||||
|  | 	schema: { | ||||||
|  | 		...InvoicesSchema, | ||||||
|  | 	}, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Initialize database schema for tests. | ||||||
|  |  */ | ||||||
|  | export async function initializeDatabase(): Promise<void> | ||||||
|  | { | ||||||
|  | 	await database.execute(sql` | ||||||
|  | DROP TABLE IF EXISTS invoices; | ||||||
|  | CREATE TABLE invoices( | ||||||
|  | 	id SERIAL PRIMARY KEY, | ||||||
|  | 	date TIMESTAMP WITH TIME ZONE, | ||||||
|  | 	amount NUMERIC(12, 2), | ||||||
|  | 	client_name VARCHAR, | ||||||
|  | 	client_address TEXT | ||||||
|  | ); | ||||||
|  | `);
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Add default invoices for tests. | ||||||
|  |  */ | ||||||
|  | export async function setupDefaultInvoices(): Promise<void> | ||||||
|  | { | ||||||
|  | 	await database.execute(sql`TRUNCATE invoices RESTART IDENTITY;`); | ||||||
|  | 	await database.insert(InvoicesSchema.invoices).values([ | ||||||
|  | 		{ | ||||||
|  | 			date: new Date("1997-10-09"), | ||||||
|  | 			amount: "5450.12", | ||||||
|  | 			clientName: "test name", | ||||||
|  | 			clientAddress: "test test test", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			date: new Date(), | ||||||
|  | 			amount: "1122.54", | ||||||
|  | 			clientName: "another name", | ||||||
|  | 			clientAddress: "foo bar baz", | ||||||
|  | 		}, | ||||||
|  | 	]); | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								tests/schema/invoices.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								tests/schema/invoices.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | import {numeric, pgTable, serial, text, timestamp, varchar} from "drizzle-orm/pg-core"; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Invoices example table with Drizzle. | ||||||
|  |  * @see https://orm.drizzle.team/docs/sql-schema-declaration
 | ||||||
|  |  */ | ||||||
|  | export const invoices = pgTable("invoices", { | ||||||
|  | 	id: serial("id").primaryKey(), | ||||||
|  | 	date: timestamp("date", { withTimezone: true }).notNull(), | ||||||
|  | 	amount: numeric("amount", { precision: 12, scale: 2 }).notNull(), | ||||||
|  | 	clientName: varchar("client_name").notNull(), | ||||||
|  | 	clientAddress: text("client_address"), | ||||||
|  | }); | ||||||
							
								
								
									
										3
									
								
								tests/setup.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								tests/setup.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | import {initializeDatabase} from "./database"; | ||||||
|  | 
 | ||||||
|  | initializeDatabase(); | ||||||
							
								
								
									
										32
									
								
								tsconfig.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								tsconfig.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | ||||||
|  | { | ||||||
|  |   "ts-node": { | ||||||
|  |     "compilerOptions": { | ||||||
|  |       "module": "ESNext", | ||||||
|  | 	    "types": ["node"], | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   "compilerOptions": { | ||||||
|  |     "outDir": "./lib/", | ||||||
|  | 	  "incremental": true, | ||||||
|  | 	  "sourceMap": true, | ||||||
|  | 	  "skipLibCheck": true, | ||||||
|  | 	  "strictNullChecks": true, | ||||||
|  | 	  "noImplicitAny": true, | ||||||
|  | 	  "noImplicitThis": true, | ||||||
|  | 	  "esModuleInterop": true, | ||||||
|  | 	  "allowSyntheticDefaultImports": true, | ||||||
|  | 	  "resolveJsonModule": true, | ||||||
|  | 	  "experimentalDecorators": true, | ||||||
|  | 	  "emitDecoratorMetadata": true, | ||||||
|  | 	  "declaration": true, | ||||||
|  |     "declarationMap": true, | ||||||
|  |     "module": "ES6", | ||||||
|  | 	  "target": "ES6", | ||||||
|  |     "moduleResolution": "Bundler", | ||||||
|  |     "lib": [ | ||||||
|  |       "ESNext", | ||||||
|  |       "DOM" | ||||||
|  |     ] | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -14,6 +14,14 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => { | ||||||
| 				formats: ["es"], | 				formats: ["es"], | ||||||
| 				fileName: "index", | 				fileName: "index", | ||||||
| 			}, | 			}, | ||||||
|  | 			rollupOptions: { | ||||||
|  | 				external: ["@sharkitek/core", "drizzle-orm"], | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		test: { | ||||||
|  | 			root: "./tests", | ||||||
|  | 			setupFiles: ["./tests/setup.ts"], | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		plugins: [ | 		plugins: [ | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue