quickpostgres
Version:
An easy, beginner-friendly PostgreSQL database wrapper similar to quick.db.
405 lines (340 loc) • 11.5 kB
text/typescript
import EventEmitter from "events";
import { Client as PgClient } from "pg";
import add from "./methods/add";
import all from "./methods/all";
import clear from "./methods/clear";
import del from "./methods/delete";
import drop from "./methods/drop";
import fetch from "./methods/fetch";
import has from "./methods/has";
import push from "./methods/push";
import set from "./methods/set";
import subtract from "./methods/subtract";
import type from "./methods/type";
export type ClientOptions = {
target?: string | null;
table?: string | null;
};
export type Params = {
id?: any;
data?: any;
ops: Options;
};
export type Options = {
table?: string;
target?: any[];
};
export type Methods =
| "fetch"
| "set"
| "add"
| "subtract"
| "push"
| "delete"
| "has"
| "clear"
| "drop"
| "all"
| "type";
export class Client extends EventEmitter {
/**
* PostgreSQL database url
* @type {string}
* @readonly
*/
readonly dbUrl: string;
/**
* Client options
* @type {ClientOptions | undefined}
* @readonly
*/
readonly options: ClientOptions | undefined;
/**
* Default table name to use
* @type {string | undefined}
*/
public tableName: string | undefined;
/**
* The `pg` client instance for your database
* @type {PgClient}
*/
public client: PgClient;
/**
* Whether the database has been connected or not
* @see connect
* @see end
*/
public connected: boolean;
constructor(dbUrl: string, options?: ClientOptions) {
super();
this.dbUrl = dbUrl;
this.options = options;
this.tableName = options?.table ?? undefined;
this.client = new PgClient(this.dbUrl);
this.connected = false;
}
/**
* Connects to the database
* @returns {Promise<Client>} the client
* @example await db.connect();
*/
public async connect(): Promise<Client> {
if (this.connected) throw new Error("Client has already been connected.");
await this.client.connect();
this.connected = true;
/**
* Emitted when the database has been connected
* @event Client#ready
* @param {Client} client the client
*/
this.emit("ready", this);
return this;
}
/**
* Ends the connection to the database
* @returns {Promise<Client>} the client
* @example await db.end();
*/
public async end(): Promise<Client> {
if (!this.connected) throw new Error("Client has not been connected yet.");
await this.client.end();
this.connected = false;
/**
* Emitted when the database connection has been ended
* @event Client#end
* @param {Client} client the client
*/
this.emit("end", this);
return this;
}
/**
* Fetches data from a key in the database
* @param {string} key any string as a key, allows dot notation
* @param {Options} options any options to be added to the request
* @returns {Promise<any>} the data requested
* @alias Client#get
* @example const data = await db.fetch("users.1234567890.inventory");
*/
public async fetch(key: string, ops?: Options): Promise<any> {
if (!key) throw new TypeError("No key specified.");
return await this.arbitrate(fetch, { id: key, ops: ops || {} });
}
/**
* Fetches data from a key in the database
* @param {string} key any string as a key, allows dot notation
* @param {options} options any options to be added to the request
* @returns {Promise<any>} the data requested
* @alias Client#fetch
* @example const data = await db.fetch("users.1234567890.inventory");
*/
public async get(key: string, ops?: Options): Promise<any> {
return await this.fetch(key, ops);
}
/**
* Sets new data based on a key in the database
* @param {string} key any string as a key, allows dot notation
* @param value value of the data to be set
* @param {Options} options any options to be added to the request
* @returns {Promise<any>} the updated data
* @example const data = await db.set("users.1234567890.level", 100);
*/
public async set(key: string, value: any, ops?: Options): Promise<any> {
if (!key) throw new TypeError("No key specified.");
return await this.arbitrate(set, {
id: key,
data: value,
ops: ops || {},
});
}
/**
* Adds a number to a key in the database. If no existing number, it will add to 0
* @param {string} key any string as a key, allows dot notation
* @param value value to add
* @param {Options} options any options to be added to the request
* @returns {Promise<any>} the updated data
* @example const data = await db.add("users.1234567890.level", 1);
*/
public async add(key: string, value: any, ops?: Options): Promise<any> {
if (!key) throw new TypeError("No key specified.");
if (isNaN(value)) throw new TypeError("No value specified to add.");
return await this.arbitrate(add, {
id: key,
data: value,
ops: ops || {},
});
}
/**
* Subtracts a number to a key in the database. If no existing number, it will subtract to 0
* @param {string} key any string as a key, allows dot notation
* @param value value to subtract
* @param {Options} options any options to be added to the request
* @returns {Promise<any>} the updated data
* @example const data = await db.subtract("users.1234567890.level", 10);
*/
public async subtract(key: string, value: any, ops?: Options): Promise<any> {
if (!key) throw new TypeError("No key specified.");
if (isNaN(value)) throw new TypeError("No value specified to subtract.");
return await this.arbitrate(subtract, {
id: key,
data: value,
ops: ops || {},
});
}
/**
* Push into an array in the database based on the key. If no existing array, it will create one
* @param {string} key any string as a key, allows dot notation
* @param value value to push
* @param {Options} options any options to be added to the request
* @returns {Promise<any>} the updated data
* @example const data = await db.push("users.1234567890.inventory", "Slice of Cheese");
*/
public async push(key: string, value: any, ops?: Options): Promise<any> {
if (!key) throw new TypeError("No key specified.");
if (!value && value != 0)
throw new TypeError("No value specified to push.");
return await this.arbitrate(push, {
id: key,
data: value,
ops: ops || {},
});
}
/**
* Delete an object (or property) in the database
* @param {string} key any string as a key, allows dot notation
* @param {Options} options any options to be added to the request
* @returns {boolean} `true` if success, if not found `false`
* @example await db.delete("users.1234567890");
*/
public async delete(key: string, ops?: Options): Promise<boolean> {
if (!key) throw new TypeError("No key specified.");
return await this.arbitrate(del, { id: key, ops: ops || {} });
}
/**
* Returns a boolean indicating whether an element with the specified key exists or not
* @param {string} key any string as a key, allows dot notation
* @param {Options} options any options to be added to the request
* @returns {boolean} boolean
* @alias Client#includes
* @example const data = await db.has("users.1234567890");
*/
public async has(key: string, ops?: Options): Promise<boolean> {
if (!key) throw new TypeError("No key specified.");
return await this.arbitrate(has, { id: key, ops: ops || {} });
}
/**
* Returns a boolean indicating whether an element with the specified key exists or not.
* @param {string} key any string as a key, allows dot notation
* @param {Options} options any options to be added to the request
* @returns {Promise<boolean>} boolean
* @alias Client#has
* @example const data = await db.has("users.1234567890");
*/
public async includes(key: string, ops?: Options): Promise<boolean> {
return await this.has(key, ops);
}
/**
* Deletes all rows from the entire active table.
* Note: This does not delete the table itself. To delete the table itself along with the rows, use `drop()`.
* @returns {Promise<number>} amount of rows deleted
* @example const data = await db.clear();
*/
public async clear(): Promise<number> {
return await this.arbitrate(clear, { ops: {} }, this.tableName);
}
/**
* Deletes the entire active table.
* @returns {Promise<void>} void
* @example await db.drop();
*/
public async drop(): Promise<void> {
return await this.arbitrate(drop, { ops: {} }, this.tableName);
}
/**
* Fetches the entire active table
* @param {Options} options any options to be added to the request
* @returns {Promise<any>} entire table as an object
* @alias Client#fetchAll
* @example const data = await db.all();
*/
public async all(ops?: Options): Promise<any> {
return await this.arbitrate(all, { ops: ops || {} });
}
/**
* Fetches the entire active table
* @param {Options} options any options to be added to the request
* @returns {Promise<any>} entire table as an object
* @alias Client#all
* @example const data = await db.all();
*/
public async fetchAll(ops?: Options): Promise<any> {
return await this.arbitrate(all, { ops: ops || {} });
}
/**
* Used to get the type of the value
* @param {string} key any string as a key, allows dot notation
* @param {Options} options any options to be added to the request
* @returns {Promise<"bigint" | "boolean" | "function" | "number" | "object" | "string" | "symbol" | "undefined">} type from `typeof`
*/
public async type(
key: string,
ops?: Options
): Promise<
| "bigint"
| "boolean"
| "function"
| "number"
| "object"
| "string"
| "symbol"
| "undefined"
> {
if (!key) throw new TypeError("No key specified.");
return await this.arbitrate(type, { id: key, ops: ops || {} });
}
/**
* @private Arbitrate
* @param {PgClient} client
* @param {Params} params
* @param {Options} options
*/
private async arbitrate(
method: (client: PgClient, params: Params, ops: Options) => any,
params: Params,
tableName?: string
): Promise<any> {
if (!this.connected)
throw new Error("Database is not connected. Use `connect()` to connect.");
if (typeof params.id == "number") params.id = params.id.toString();
const options = {
table:
this.options?.table || params.ops.table || tableName || "quickpostgres",
};
// Access database
await this.client.query(
`CREATE TABLE IF NOT EXISTS ${options.table} (ID TEXT, json TEXT)`
);
// Verify options
if (params.ops.target && params.ops.target[0] === ".")
params.ops.target = params.ops.target.slice(1);
if (params.data && params.data === Infinity)
throw new TypeError(
`Infinity cannot be used as a field. (ID: ${params.id})`
);
// Stringify
try {
params.data = JSON.stringify(params.data);
} catch (error) {
throw new TypeError(
`Please supply a valid input. (ID: ${params.id})\nError: ${error}`
);
}
// Translate dot notation from keys
if (params.id?.includes(".")) {
const unparsed = params.id.split(".");
params.id = unparsed.shift();
params.ops.target = unparsed.join(".");
}
// Run and return method
return await method(this.client, params, options);
}
}