Organize directories
This commit is contained in:
parent
8a91c9b89b
commit
4898382f78
433 changed files with 0 additions and 0 deletions
30
packages/metamigo-common/records/account.ts
Normal file
30
packages/metamigo-common/records/account.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { recordInfo } from "./record-info";
|
||||
import { RepositoryBase } from "./base";
|
||||
import { Flavor, UUID } from "../helpers";
|
||||
import { UserId } from "./user";
|
||||
|
||||
export type AccountId = Flavor<UUID, "Account Id">;
|
||||
|
||||
export interface UnsavedAccount {
|
||||
compoundId: string;
|
||||
userId: UserId;
|
||||
providerType: string;
|
||||
providerId: string;
|
||||
providerAccountId: string;
|
||||
refreshToken: string;
|
||||
accessToken: string;
|
||||
accessTokenExpires: Date;
|
||||
}
|
||||
|
||||
export interface SavedAccount extends UnsavedAccount {
|
||||
id: AccountId;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export const AccountRecord = recordInfo<UnsavedAccount, SavedAccount>(
|
||||
"app_public",
|
||||
"accounts"
|
||||
);
|
||||
|
||||
export class AccountRecordRepository extends RepositoryBase(AccountRecord) {}
|
||||
57
packages/metamigo-common/records/base.ts
Normal file
57
packages/metamigo-common/records/base.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { TableName } from "pg-promise";
|
||||
import { IMain } from "../db/types";
|
||||
import { CrudRepository } from "./crud-repository";
|
||||
import { PgRecordInfo, UnsavedR, SavedR, KeyType } from "./record-info";
|
||||
import type { IDatabase } from "pg-promise";
|
||||
|
||||
export type PgProtocol<T> = IDatabase<T> & T;
|
||||
|
||||
/**
|
||||
* This function returns a constructor for a repository class for [[TRecordInfo]]
|
||||
*
|
||||
* @param aRecordType the record type runtime definition
|
||||
*/
|
||||
// haven't figured out a good return type for this function
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export function unboundRepositoryBase<
|
||||
TRecordInfo extends PgRecordInfo,
|
||||
TDatabaseExtension
|
||||
>(aRecordType: TRecordInfo) {
|
||||
return class Repository extends CrudRepository<
|
||||
UnsavedR<TRecordInfo>,
|
||||
SavedR<TRecordInfo>,
|
||||
KeyType<TRecordInfo>
|
||||
> {
|
||||
_recordType!: TRecordInfo;
|
||||
static readonly recordType = aRecordType;
|
||||
static readonly schemaName = aRecordType.schemaName;
|
||||
static readonly tableName = aRecordType.tableName;
|
||||
public readonly recordType = aRecordType;
|
||||
public readonly schemaTable: TableName;
|
||||
public db: PgProtocol<TDatabaseExtension>;
|
||||
public pgp: IMain;
|
||||
|
||||
constructor(db: PgProtocol<TDatabaseExtension>) {
|
||||
super();
|
||||
|
||||
this.pgp = db.$config.pgp;
|
||||
this.schemaTable = new this.pgp.helpers.TableName({
|
||||
schema: aRecordType.schemaName,
|
||||
table: aRecordType.tableName,
|
||||
});
|
||||
|
||||
this.db = db;
|
||||
if (!this.db) {
|
||||
throw new Error("Missing database in repository");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export function RepositoryBase<
|
||||
Rec extends PgRecordInfo,
|
||||
TDatabaseExtension = unknown
|
||||
>(recordType: Rec) {
|
||||
return unboundRepositoryBase<Rec, TDatabaseExtension>(recordType);
|
||||
}
|
||||
321
packages/metamigo-common/records/crud-repository.ts
Normal file
321
packages/metamigo-common/records/crud-repository.ts
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
/* eslint-disable @typescript-eslint/ban-types,@typescript-eslint/no-explicit-any */
|
||||
import { TableName } from "pg-promise";
|
||||
import decamelcaseKeys from "decamelcase-keys";
|
||||
import isObject from "lodash/isObject";
|
||||
import isArray from "lodash/isArray";
|
||||
import zipObject from "lodash/zipObject";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import omit from "lodash/omit";
|
||||
import { IDatabase, IMain, IResult } from "../db/types";
|
||||
import { PgRecordInfo, idKeysOf } from "./record-info";
|
||||
|
||||
export interface ICrudRepository<
|
||||
TUnsavedR,
|
||||
TSavedR extends TUnsavedR & IdKeyT,
|
||||
IdKeyT extends object
|
||||
> {
|
||||
findById(id: IdKeyT): Promise<TSavedR | null>;
|
||||
findBy(example: Partial<TSavedR>): Promise<TSavedR | null>;
|
||||
findAll(): Promise<TSavedR[]>;
|
||||
findAllBy(example: Partial<TSavedR>): Promise<TSavedR[]>;
|
||||
existsById(id: IdKeyT): Promise<boolean>;
|
||||
countBy(example: Partial<TSavedR>): Promise<number>;
|
||||
count(): Promise<number>;
|
||||
insert(record: TUnsavedR): Promise<TSavedR>;
|
||||
insertAll(toInsert: TUnsavedR[]): Promise<TSavedR[]>;
|
||||
updateById(id: IdKeyT, attrs: Partial<TSavedR>): Promise<TSavedR>;
|
||||
update(record: TSavedR): Promise<TSavedR>;
|
||||
updateAll(toUpdate: TSavedR[]): Promise<TSavedR[]>;
|
||||
remove(record: TSavedR): Promise<number>;
|
||||
removeAll(toRemove: TSavedR[]): Promise<number>;
|
||||
removeBy(example: Partial<TSavedR>): Promise<TSavedR | null>;
|
||||
removeById(id: IdKeyT): Promise<number>;
|
||||
}
|
||||
|
||||
// The snake cased object going into the db
|
||||
type DatabaseRow = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Base class for generic CRUD operations on a repository for a specific type.
|
||||
*
|
||||
* Several assumptions are made about your environment for this generic CRUD repository to work:
|
||||
*
|
||||
* - the underlying column names are snake_cased (this behavior can be changed, see [[columnize]])
|
||||
* - the rows have only a single primary key (composite keys are not supported)
|
||||
*
|
||||
* @typeParam ID The type of the id column
|
||||
* @typeParam T The type of the record
|
||||
*/
|
||||
export abstract class CrudRepository<
|
||||
TUnsavedR,
|
||||
TSavedR extends TUnsavedR & IdKeyT,
|
||||
IdKeyT extends object
|
||||
> implements ICrudRepository<TUnsavedR, TSavedR, IdKeyT>
|
||||
{
|
||||
/**
|
||||
* the fully qualified table name
|
||||
*/
|
||||
abstract schemaTable: TableName;
|
||||
abstract recordType: PgRecordInfo<TUnsavedR, TSavedR, IdKeyT>;
|
||||
abstract db: IDatabase;
|
||||
abstract pgp: IMain;
|
||||
|
||||
/**
|
||||
* Converts the record's columns into snake_case
|
||||
*
|
||||
* @param record the record of type T to convert
|
||||
*/
|
||||
columnize(record: TSavedR | Partial<TSavedR>): DatabaseRow {
|
||||
return decamelcaseKeys(record);
|
||||
}
|
||||
|
||||
/*
|
||||
* Creates a simple where clause with each key-value in `example` is
|
||||
* formatted as KEY=VALUE and all kv-pairs are ANDed together.
|
||||
*
|
||||
* @param example key value pair of column names and values
|
||||
*/
|
||||
where(example: Partial<TSavedR>): string {
|
||||
const snaked = this.columnize(example);
|
||||
const clauses = Object.keys(snaked).reduce((acc, cur) => {
|
||||
const colName = this.pgp.as.format("$1:name", cur);
|
||||
return `${acc} and ${colName} = $<${cur}>`;
|
||||
}, "");
|
||||
const where = this.pgp.as.format(`WHERE 1=1 ${clauses}`, { ...snaked }); // Pre-format WHERE condition
|
||||
return where;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a value containing the id of the record (which could be a primitive type, a composite object, or an array of values)
|
||||
* into an object which can be safely passed to [[where]].
|
||||
*/
|
||||
idsObj(idValues: IdKeyT): IdKeyT {
|
||||
if (isEmpty(idValues)) {
|
||||
throw new Error(`idsObj(${this.schemaTable}): passed empty id(s)`);
|
||||
}
|
||||
|
||||
let ids = {};
|
||||
const idKeys = idKeysOf(this.recordType as any);
|
||||
if (isArray(idValues)) {
|
||||
ids = zipObject(idKeys, idValues);
|
||||
} else if (isObject(idValues)) {
|
||||
ids = idValues;
|
||||
} else {
|
||||
if (idKeys.length !== 1) {
|
||||
throw new Error(
|
||||
`idsObj(${this.schemaTable}): passed record has multiple primary keys. the ids must be passed as an object or array. ${idValues}`
|
||||
);
|
||||
}
|
||||
// @ts-ignore
|
||||
ids[idKeys[0]] = idValues;
|
||||
}
|
||||
|
||||
// this is a sanity check so we don't do something like
|
||||
// deleting all the data if a WHERE slips in with no ids
|
||||
if (isEmpty(ids)) {
|
||||
throw new Error(`idsObj(${this.schemaTable}): passed empty ids`);
|
||||
}
|
||||
|
||||
return ids as IdKeyT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all rows in the table
|
||||
*/
|
||||
async findAll(): Promise<TSavedR[]> {
|
||||
return this.db.any("SELECT * FROM $1", [this.schemaTable]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of rows in the table
|
||||
*/
|
||||
async count(): Promise<number> {
|
||||
return this.db.one(
|
||||
"SELECT count(*) FROM $1",
|
||||
[this.schemaTable],
|
||||
(a: { count: string }) => Number(a.count)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of rows in the table matching the example
|
||||
*/
|
||||
async countBy(example: Partial<TSavedR>): Promise<number> {
|
||||
return this.db.one(
|
||||
"SELECT count(*) FROM $1 $2:raw ",
|
||||
[this.schemaTable, this.where(example)],
|
||||
(a: { count: string }) => Number(a.count)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single row where the example are true.
|
||||
* @param example key-value pairs of column names and values
|
||||
*/
|
||||
async findBy(example: Partial<TSavedR>): Promise<TSavedR | null> {
|
||||
return this.db.oneOrNone("SELECT * FROM $1 $2:raw LIMIT 1", [
|
||||
this.schemaTable,
|
||||
this.where(example),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a row by ID
|
||||
* @param id
|
||||
*/
|
||||
async findById(id: IdKeyT): Promise<TSavedR | null> {
|
||||
const where = this.idsObj(id);
|
||||
return this.db.oneOrNone("SELECT * FROM $1 $2:raw", [
|
||||
this.schemaTable,
|
||||
this.where(where),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a given row with id exists
|
||||
* @param id
|
||||
*/
|
||||
async existsById(id: IdKeyT): Promise<boolean> {
|
||||
return this.db.one(
|
||||
"SELECT EXISTS(SELECT 1 FROM $1 $2:raw)",
|
||||
[this.schemaTable, this.where(this.idsObj(id))],
|
||||
(a: { exists: boolean }) => a.exists
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all rows where the example are true.
|
||||
* @param example key-value pairs of column names and values
|
||||
*/
|
||||
async findAllBy(example: Partial<TSavedR>): Promise<TSavedR[]> {
|
||||
return this.db.any("SELECT * FROM $1 $2:raw", [
|
||||
this.schemaTable,
|
||||
this.where(example),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new row
|
||||
* @param record
|
||||
* @return the new row
|
||||
*/
|
||||
async insert(record: TUnsavedR): Promise<TSavedR> {
|
||||
return this.db.one("INSERT INTO $1 ($2:name) VALUES ($2:csv) RETURNING *", [
|
||||
this.schemaTable,
|
||||
this.columnize(record as any),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `insert` but will insert/update a batch of rows at once
|
||||
*/
|
||||
async insertAll(toInsert: TUnsavedR[]): Promise<TSavedR[]> {
|
||||
return this.db.tx((t) => {
|
||||
const insertCommands: any[] = [];
|
||||
|
||||
toInsert.forEach((record) => {
|
||||
insertCommands.push(this.insert(record));
|
||||
});
|
||||
|
||||
return t.batch(insertCommands);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a row by id
|
||||
* @param id
|
||||
* @return the number of rows affected
|
||||
*/
|
||||
async removeById(id: IdKeyT): Promise<number> {
|
||||
return this.db.result(
|
||||
"DELETE FROM $1 $2:raw",
|
||||
[this.schemaTable, this.where(this.idsObj(id))],
|
||||
(r: IResult) => r.rowCount
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete records matching the query
|
||||
* @param example key-value pairs of column names and values
|
||||
*/
|
||||
async removeBy(example: Partial<TSavedR>): Promise<TSavedR | null> {
|
||||
if (isEmpty(example))
|
||||
throw new Error(
|
||||
`removeBy(${this.schemaTable}): passed empty constraint!`
|
||||
);
|
||||
return this.db.result("DELETE FROM $1 $2:raw", [
|
||||
this.schemaTable,
|
||||
this.where(example),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given row
|
||||
*
|
||||
* @param record to remove
|
||||
* @return the number of rows affected
|
||||
*/
|
||||
async remove(record: TSavedR): Promise<number> {
|
||||
return this.removeById(this.recordType.idOf(record));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all rows
|
||||
* @param toRemove a list of rows to remove, if empty, DELETES ALL ROWS
|
||||
* @return the number of rows affected
|
||||
*/
|
||||
async removeAll(toRemove: TSavedR[] = []): Promise<number> {
|
||||
if (toRemove.length === 0) {
|
||||
return this.db.result(
|
||||
"DELETE FROM $1 WHERE 1=1;",
|
||||
[this.schemaTable],
|
||||
(r: IResult) => r.rowCount
|
||||
);
|
||||
}
|
||||
|
||||
const results = await this.db.tx((t) => {
|
||||
const delCommands: any[] = [];
|
||||
|
||||
toRemove.forEach((record) => {
|
||||
delCommands.push(this.remove(record));
|
||||
});
|
||||
|
||||
return t.batch(delCommands);
|
||||
});
|
||||
return results.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing row
|
||||
* @param id
|
||||
* @param attrs
|
||||
* @return the updated row
|
||||
*/
|
||||
async updateById(id: IdKeyT, attrs: Partial<TSavedR>): Promise<TSavedR> {
|
||||
const idKeys = idKeysOf(this.recordType as any);
|
||||
const attrsSafe = omit(attrs, idKeys);
|
||||
return this.db.one(
|
||||
"UPDATE $1 SET ($2:name) = ROW($2:csv) $3:raw RETURNING *",
|
||||
[this.schemaTable, this.columnize(attrsSafe), this.where(this.idsObj(id))]
|
||||
);
|
||||
}
|
||||
|
||||
async update(record: TSavedR): Promise<TSavedR> {
|
||||
return this.updateById(this.recordType.idOf(record), record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a batch of records at once
|
||||
*/
|
||||
async updateAll(toUpdate: TSavedR[]): Promise<TSavedR[]> {
|
||||
return this.db.tx((t) => {
|
||||
const updateCommands: any[] = [];
|
||||
|
||||
toUpdate.forEach((record) => {
|
||||
updateCommands.push(this.update(record));
|
||||
});
|
||||
|
||||
return t.batch(updateCommands);
|
||||
});
|
||||
}
|
||||
}
|
||||
16
packages/metamigo-common/records/index.ts
Normal file
16
packages/metamigo-common/records/index.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export * from "./base";
|
||||
export * from "./record-info";
|
||||
export * from "./crud-repository";
|
||||
export * from "./user";
|
||||
export * from "./session";
|
||||
export * from "./account";
|
||||
|
||||
import type { AccountRecordRepository } from "./account";
|
||||
import type { UserRecordRepository } from "./user";
|
||||
import type { SessionRecordRepository } from "./session";
|
||||
|
||||
export interface IMetamigoRepositories {
|
||||
users: UserRecordRepository;
|
||||
sessions: SessionRecordRepository;
|
||||
accounts: AccountRecordRepository;
|
||||
}
|
||||
54
packages/metamigo-common/records/record-info.d.ts
vendored
Normal file
54
packages/metamigo-common/records/record-info.d.ts
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
export interface EntityType<TUnsaved = any, TSaved = any, TIds extends object = any> {
|
||||
_saved: TSaved;
|
||||
_unsaved: TUnsaved;
|
||||
_idKeys: TIds;
|
||||
idOf: (rec: TSaved) => TIds;
|
||||
}
|
||||
export declare type UnsavedR<T extends {
|
||||
_unsaved: any;
|
||||
}> = T["_unsaved"];
|
||||
export declare type SavedR<T extends {
|
||||
_saved: any;
|
||||
}> = T["_saved"];
|
||||
export declare type KeyType<R extends EntityType> = R["_idKeys"];
|
||||
export interface PgRecordInfo<Unsaved = any, Saved extends Unsaved & IdType = any, IdType extends object = any> extends EntityType<Unsaved, Saved, IdType> {
|
||||
tableName: string;
|
||||
schemaName: string;
|
||||
idKeys: (keyof Saved)[];
|
||||
}
|
||||
/**
|
||||
* Extract the runtime key name from a recordInfo
|
||||
*/
|
||||
export declare function idKeysOf<RI extends PgRecordInfo>(recordInfoWithIdKey: RI): string[];
|
||||
/**
|
||||
* Turns a record type with possibly more fields than "id" into an array
|
||||
*/
|
||||
export declare function collectIdValues<RecordT extends PgRecordInfo>(idObj: KeyType<RecordT>, knexRecordType: RecordT): string[];
|
||||
/**
|
||||
*
|
||||
* Creates a record descriptor that captures the table name, primary key name,
|
||||
* unsaved type, and saved type of a database record type. Assumes "id" as the
|
||||
* primary key name
|
||||
*
|
||||
*/
|
||||
export declare function recordInfo<Unsaved, Saved extends Unsaved & {
|
||||
id: any;
|
||||
}>(schemaName: string, tableName: string): PgRecordInfo<Unsaved, Saved, Pick<Saved, "id">>;
|
||||
export declare function recordInfo<Type extends {
|
||||
id: string;
|
||||
}>(schemaName: string, tableName: string): PgRecordInfo<Type, Type, Pick<Type, "id">>;
|
||||
/**
|
||||
*
|
||||
* Creates a record descriptor that captures the table name, primary key name,
|
||||
* unsaved type, and saved type of a database record type.
|
||||
*
|
||||
*/
|
||||
export declare function recordInfo<Unsaved, Saved extends Unsaved, Id extends keyof Saved>(schemaName: string, tableName: string, idKey: Id[]): PgRecordInfo<Unsaved, Saved, Pick<Saved, Id>>;
|
||||
/**
|
||||
*
|
||||
* Creates a record descriptor for records with composite primary keys
|
||||
*
|
||||
*/
|
||||
export declare function compositeRecordType<TUnsaved, TSaved extends TUnsaved = TUnsaved>(schemaName: string, tableName: string): {
|
||||
withCompositeKeys<TKeys extends keyof TSaved>(keys: TKeys[]): PgRecordInfo<TUnsaved, TSaved, Pick<TSaved, TKeys>>;
|
||||
};
|
||||
133
packages/metamigo-common/records/record-info.ts
Normal file
133
packages/metamigo-common/records/record-info.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
/* eslint-disable @typescript-eslint/ban-types,@typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types */
|
||||
import at from "lodash/at";
|
||||
import pick from "lodash/pick";
|
||||
|
||||
export interface EntityType<
|
||||
TUnsaved = any,
|
||||
TSaved = any,
|
||||
TIds extends object = any
|
||||
> {
|
||||
_saved: TSaved;
|
||||
_unsaved: TUnsaved;
|
||||
_idKeys: TIds;
|
||||
idOf: (rec: TSaved) => TIds;
|
||||
}
|
||||
|
||||
export type UnsavedR<T extends { _unsaved: any }> = T["_unsaved"];
|
||||
export type SavedR<T extends { _saved: any }> = T["_saved"];
|
||||
export type KeyType<R extends EntityType> = R["_idKeys"];
|
||||
|
||||
export interface PgRecordInfo<
|
||||
Unsaved = any,
|
||||
Saved extends Unsaved & IdType = any,
|
||||
IdType extends object = any
|
||||
> extends EntityType<Unsaved, Saved, IdType> {
|
||||
tableName: string;
|
||||
schemaName: string;
|
||||
idKeys: (keyof Saved)[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the runtime key name from a recordInfo
|
||||
*/
|
||||
export function idKeysOf<RI extends PgRecordInfo>(
|
||||
recordInfoWithIdKey: RI
|
||||
): string[] {
|
||||
return recordInfoWithIdKey.idKeys as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a record type with possibly more fields than "id" into an array
|
||||
*/
|
||||
export function collectIdValues<RecordT extends PgRecordInfo>(
|
||||
idObj: KeyType<RecordT>,
|
||||
knexRecordType: RecordT
|
||||
): string[] {
|
||||
return at(idObj, idKeysOf(knexRecordType));
|
||||
}
|
||||
|
||||
function castToRecordInfo(
|
||||
runtimeData: Omit<PgRecordInfo, "_idKeys" | "_saved" | "_unsaved">
|
||||
): PgRecordInfo {
|
||||
return runtimeData as PgRecordInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Creates a record descriptor that captures the table name, primary key name,
|
||||
* unsaved type, and saved type of a database record type. Assumes "id" as the
|
||||
* primary key name
|
||||
*
|
||||
*/
|
||||
export function recordInfo<Unsaved, Saved extends Unsaved & { id: any }>(
|
||||
schemaName: string,
|
||||
tableName: string
|
||||
): PgRecordInfo<Unsaved, Saved, Pick<Saved, "id">>;
|
||||
|
||||
export function recordInfo<Type extends { id: string }>(
|
||||
schemaName: string,
|
||||
tableName: string
|
||||
): PgRecordInfo<Type, Type, Pick<Type, "id">>;
|
||||
|
||||
/**
|
||||
*
|
||||
* Creates a record descriptor that captures the table name, primary key name,
|
||||
* unsaved type, and saved type of a database record type.
|
||||
*
|
||||
*/
|
||||
export function recordInfo<
|
||||
Unsaved,
|
||||
Saved extends Unsaved,
|
||||
Id extends keyof Saved
|
||||
>(
|
||||
schemaName: string,
|
||||
tableName: string,
|
||||
idKey: Id[]
|
||||
): PgRecordInfo<Unsaved, Saved, Pick<Saved, Id>>;
|
||||
|
||||
/**
|
||||
*
|
||||
* Don't use this signature be sure to provide unsaved and saved types.
|
||||
*
|
||||
*/
|
||||
export function recordInfo(
|
||||
schemaName: string,
|
||||
tableName: string,
|
||||
idKeys?: string[]
|
||||
) {
|
||||
idKeys = idKeys || ["id"];
|
||||
return castToRecordInfo({
|
||||
schemaName,
|
||||
tableName,
|
||||
idKeys,
|
||||
idOf: (rec) => pick(rec, idKeys as any),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Creates a record descriptor for records with composite primary keys
|
||||
*
|
||||
*/
|
||||
export function compositeRecordType<
|
||||
TUnsaved,
|
||||
TSaved extends TUnsaved = TUnsaved
|
||||
>(
|
||||
schemaName: string,
|
||||
tableName: string
|
||||
): {
|
||||
withCompositeKeys<TKeys extends keyof TSaved>(
|
||||
keys: TKeys[]
|
||||
): PgRecordInfo<TUnsaved, TSaved, Pick<TSaved, TKeys>>;
|
||||
} {
|
||||
return {
|
||||
withCompositeKeys(keys) {
|
||||
return castToRecordInfo({
|
||||
schemaName,
|
||||
tableName,
|
||||
idKeys: keys,
|
||||
idOf: (rec) => pick(rec, keys),
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
26
packages/metamigo-common/records/session.ts
Normal file
26
packages/metamigo-common/records/session.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { recordInfo } from "./record-info";
|
||||
import { RepositoryBase } from "./base";
|
||||
import { Flavor, UUID } from "../helpers";
|
||||
import { UserId } from "./user";
|
||||
|
||||
export type SessionId = Flavor<UUID, "Session Id">;
|
||||
|
||||
export interface UnsavedSession {
|
||||
userId: UserId;
|
||||
expires: Date;
|
||||
sessionToken: string;
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
export interface SavedSession extends UnsavedSession {
|
||||
id: SessionId;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export const SessionRecord = recordInfo<UnsavedSession, SavedSession>(
|
||||
"app_private",
|
||||
"sessions"
|
||||
);
|
||||
|
||||
export class SessionRecordRepository extends RepositoryBase(SessionRecord) {}
|
||||
40
packages/metamigo-common/records/user.ts
Normal file
40
packages/metamigo-common/records/user.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { recordInfo } from "./record-info";
|
||||
import { RepositoryBase } from "./base";
|
||||
import { Flavor, UUID } from "../helpers";
|
||||
|
||||
export type UserId = Flavor<UUID, "User Id">;
|
||||
|
||||
export interface UnsavedUser {
|
||||
name: string;
|
||||
email: string;
|
||||
emailVerified: Date;
|
||||
avatar: string;
|
||||
isActive: boolean;
|
||||
userRole: string;
|
||||
}
|
||||
|
||||
export interface SavedUser extends UnsavedUser {
|
||||
id: UserId;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export const UserRecord = recordInfo<UnsavedUser, SavedUser>(
|
||||
"app_public",
|
||||
"users"
|
||||
);
|
||||
|
||||
export class UserRecordRepository extends RepositoryBase(UserRecord) {
|
||||
async upsert(record: UnsavedUser | SavedUser): Promise<SavedUser> {
|
||||
return this.db.one(
|
||||
`INSERT INTO $1 ($2:name) VALUES ($2:csv)
|
||||
ON CONFLICT (email)
|
||||
DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
avatar = EXCLUDED.avatar,
|
||||
email_verified = EXCLUDED.email_verified
|
||||
RETURNING *`,
|
||||
[this.schemaTable, this.columnize(record)]
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue