UNPKG

qb-record

Version:

A lightweight abstraction layer for Quick Base

593 lines (474 loc) 13.2 kB
'use strict'; /* Dependencies */ import merge from 'deepmerge'; import RFC4122 from 'rfc4122'; import { QuickBase, QuickBaseError, QuickBaseOptions, QuickBaseRequest, QuickBaseResponseDeleteRecords } from 'quickbase'; import { QBField, QBFieldJSON } from 'qb-field'; /* Globals */ const VERSION = require('../package.json').version; const IS_BROWSER = typeof(window) !== 'undefined'; const rfc4122 = new RFC4122(); /* Main Class */ export class QBRecord<RecordData extends QBRecordData = QBRecordData> { public readonly CLASS_NAME = 'QBRecord'; static readonly CLASS_NAME = 'QBRecord'; static readonly VERSION: string = VERSION; /** * The default settings of a `QuickBase` instance */ static defaults: QBRecordOptions = { quickbase: { realm: IS_BROWSER ? window.location.host.split('.')[0] : '' }, tableId: (() => { if(IS_BROWSER){ const tableId = window.location.pathname.match(/^\/db\/(?!main)(.*)$/); if(tableId){ return tableId[1]; } } return ''; })(), fids: { recordid: 3, primaryKey: 3 }, recordid: undefined, primaryKey: undefined }; /** * An internal id (guid) used for tracking/managing object instances */ public id: string; private _qb: QuickBase; private _tableId: string = ''; private _fids: Record<any, number> = {}; private _fields: QBField[] = []; private _data: Record<any, any> = {}; constructor(options?: Partial<QBRecordOptions<RecordData>>){ this.id = rfc4122.v4(); const { quickbase, ...classOptions } = options || {}; if(QuickBase.IsQuickBase(quickbase)){ this._qb = quickbase; }else{ this._qb = new QuickBase(merge.all([ QBRecord.defaults.quickbase, quickbase || {} ])); } const settings = merge(QBRecord.defaults, classOptions); this.setTableId(settings.tableId) .setFids(settings.fids as Record<any, number>) // @ts-ignore - my typescript skills fail me for now, tests are fine though .set('recordid', settings.recordid) // @ts-ignore - my typescript skills fail me for now, tests are fine though .set('primaryKey', settings.primaryKey); return this; } clear(): this { this._data = {}; this._fields = []; return this; } async delete({ requestOptions }: QuickBaseRequest = {}): Promise<QuickBaseResponseDeleteRecords> { const recordid = this.get('recordid'); if(recordid){ const results = await this._qb.deleteRecords({ tableId: this.getTableId(), where: `{'${this.getFid('recordid')}'.EX.'${recordid}'}`, requestOptions }); if(results.numberDeleted !== 0){ this.clear(); } return results; }else{ this.clear(); return { numberDeleted: 0 }; } } get<F extends keyof RecordData>(field: F): RecordData[F]; get<F extends string>(field: F): F extends keyof RecordData ? RecordData[F] : any; get(field: any): any { return this._data[field]; } getFid<T extends keyof RecordData>(field: T): number; getFid(field: number, byId: true): string; getFid(field: string | number, byId?: false): number; getFid(field: string | number, byId: boolean = false): string | number { const fids = this.getFids(); let id: string | number = -1; if(byId !== true){ if(fids.hasOwnProperty(field)){ id = fids[field]; } }else{ id = ''; field = +field; Object.entries(fids).some(([ name, fid ]) => { if(fid === field){ id = name; return true; } return false; }); } return id; } getFids(): QBFids<RecordData> { return this._fids as QBFids<RecordData>; } getField(id: number, returnIndex: true): number | undefined; getField(id: number, returnIndex?: false): QBField | undefined; getField(id: number, returnIndex: boolean = false): number | QBField | undefined { const fields = this.getFields(); let result = undefined; for(let i = 0; result === undefined && i < fields.length; ++i){ if(fields[i].getFid() === id){ result = returnIndex ? i : fields[i]; } } return result; } getFields(): QBField[] { return this._fields; } getTableId(): string { return this._tableId; } async getTempToken({ requestOptions }: QuickBaseRequest = {}): Promise<void> { await this._qb.getTempTokenDBID({ dbid: this.getTableId(), requestOptions }); } async load({ clist, query, requestOptions }: QBRecordLoad = {}): Promise<Record<any, any>> { const where = []; let fids = this.getFids() as Record<any, any>; let select = []; if(query){ where.push(`(${query})`); } if(this.get('recordid')){ where.push(`{'${this.getFid('recordid')}'.EX.'${this.get('recordid')}'}`); }else if(this.get('primaryKey')){ where.push(`{'${this.getFid('primaryKey')}'.EX.'${this.get('primaryKey')}'}`); } if(clist){ if(typeof(clist) === 'string'){ select = clist.split('.').map((val: string) => +val); }else{ select = clist; } fids = select.reduce((fids, fid) => { let name: string | number = this.getFid(fid, true); if(!name){ name = fid; this.setFid(fid, fid); } fids[name] = fid; return fids; }, {} as Record<any, any>); }else{ select = Object.entries(fids).map((fid) => { return fid[1]; }); } const results = await this._qb.runQuery({ tableId: this.getTableId(), where: where.join('AND'), select: select, requestOptions }); results.fields.forEach((field) => { let result = this.getField(field.id); if(!result){ result = new QBField({ quickbase: this._qb, tableId: this.getTableId(), fid: field.id }); this._fields.push(result); } Object.entries(field).forEach(([ attribute, property ]) => { result!.set(attribute, property); }); }); if(results.metadata.totalRecords === 0){ throw new Error('Record not found'); } const record = results.data[0]; Object.entries(fids).forEach(([ name, fid ]) => { this.set(name, record[fid].value); }); return this._data; } async loadSchema({ requestOptions }: QuickBaseRequest = {}): Promise<QBField[]> { const results = await this._qb.getFields({ tableId: this.getTableId(), requestOptions }); results.forEach((field) => { let result = this.getField(field.id); if(result === undefined){ result = new QBField({ quickbase: this._qb, tableId: this.getTableId(), fid: field.id }); this._fields.push(result); } Object.entries(field).forEach(([ property, value ]) => { result!.set(property, value); }); }); return this.getFields(); } async save({ fidsToSave, mergeFieldId, requestOptions }: QuickBaseRequest & { fidsToSave?: (keyof RecordData | number)[]; mergeFieldId?: number; } = {}): Promise<Record<any, any>> { const fids = this.getFids(); const names = Object.entries(fids).map(([ name ]) => name); const mergeField = mergeFieldId || this.getFid('primaryKey'); const { headers, data: results } = await this._qb.upsert({ tableId: this.getTableId(), mergeFieldId: mergeField, data: [names.filter((name) => { const fid = fids[name]; const filtered = !fidsToSave || fidsToSave.indexOf(fid) !== -1 || fidsToSave.indexOf(name) !== -1 || fid === mergeField; if(!filtered){ return false; } const field = this.getField(fid); if(field && [ 'lookup', 'summary', 'formula' ].indexOf(field.get('mode') || '') !== -1){ return false; } return true; }).reduce((record, name) => { const fid = fids[name]; if(fid){ record[fid] = { value: replaceUndefinedWithString(this.get(name)) }; } return record; }, {} as Record<string, { value: any }>)], fieldsToReturn: names.map((name) => { return fids[name]; }).filter((val, i, arr) => { return arr.indexOf(val) === i; }), requestOptions, returnAxios: true }); const error = typeof(results.metadata.lineErrors) !== 'undefined' ? results.metadata.lineErrors[1] : false; if(error){ throw new QuickBaseError(207, 'A partial success response was returned', error.join('\n'), headers['qb-api-ray']); } const record = results.data[0]; names.forEach((name) => { let value = undefined; const fid = fids[name]; if(fid){ const field = record[fid]; if(field){ value = field.value; } } this.set(name, value); }); return this._data; } set<F extends keyof RecordData>(field: F, value: RecordData[F]): this; set<F extends string>(field: F, value: F extends keyof RecordData ? RecordData[F] : any): this; set(field: any, value: any): this { this._data[field] = value; return this; } setTableId(tableId: string): this { this._tableId = tableId; return this; } setFid<T extends keyof RecordData>(name: T, id: number): this; setFid(name: string | number, id: number): this; setFid(name: string | number, id: number): this { this._fids[name] = +id; return this; } setFids(fields: Record<any, number>): this { Object.entries(fields).forEach(([ name, fid ]) => { this.setFid(name, fid); }); return this; } setFields(fields: QBField[]): this { this._fields = fields; return this; } /** * Rebuild the QBRecord instance from serialized JSON * * @param json QBRecord serialized JSON */ fromJSON(json: string | QBRecordJSON<RecordData>): this { if(typeof(json) === 'string'){ json = JSON.parse(json); } if(typeof(json) !== 'object'){ throw new TypeError('json argument must be type of object or a valid JSON string'); } if(json.quickbase){ this._qb = new QuickBase(json.quickbase); } if(json.tableId){ this.setTableId(json.tableId); } if(json.fids){ this.setFids(json.fids); } if(json.recordid){ // @ts-ignore - my typescript skills fail me for now, tests are fine though this.set('recordid', json.recordid); } if(json.primaryKey){ // @ts-ignore - my typescript skills fail me for now, tests are fine though this.set('primaryKey', json.primaryKey); } if(json.fields){ json.fields.forEach((fieldJSON) => { this._fields.push(QBField.fromJSON(fieldJSON)); }); } if(json.data){ Object.entries(json.data).forEach(([ property, value ]) => { this.set(property, value); }); } return this; } /** * Serialize the QBRecord instance into JSON */ toJSON(fidsToConvert?: (string | number)[]): QBRecordJSON<RecordData> { return { quickbase: this._qb.toJSON(), tableId: this.getTableId(), fids: this.getFids(), recordid: this.get('recordid'), primaryKey: this.get('primaryKey'), fields: this.getFields().map((field) => { return field.toJSON(); }), data: Object.entries(this._data).filter(([ name ]) => { return !fidsToConvert || fidsToConvert.indexOf(name) !== -1; }).reduce((data, [ name, value ]) => { data[name] = value; return data; }, {} as Record<any, any>) }; } /** * Create a new QBRecord instance from serialized JSON * * @param json QBRecord serialized JSON */ static fromJSON<T extends QBRecordData = QBRecordData>(json: string | QBRecordJSON<T>): QBRecord<T> { if(typeof(json) === 'string'){ json = JSON.parse(json); } if(typeof(json) !== 'object'){ throw new TypeError('json argument must be type of object or a valid JSON string'); } const newRecord = new QBRecord(); return newRecord.fromJSON(json); } /** * Test if a variable is a `qb-record` object * * @param obj A variable you'd like to test */ static IsQBRecord<T extends QBRecordData = QBRecordData>(obj: any): obj is QBRecord<T> { return ((obj || {}) as QBRecord).CLASS_NAME === QBRecord.CLASS_NAME; } /** * Returns a new QBRecord instance built off of `options`, that inherits configuration data from the passed in `data` argument. * * @param options QBRecord instance options * @param data Quick Base Record data */ static NewRecord<T extends QBRecordData = QBRecordData>(options: Partial<QBRecordOptions<T>>, data?: Partial<T>): QBRecord<T> { const newRecord = new QBRecord<T>(options); if(data){ Object.entries(data).forEach(([ property, value ]) => { newRecord.set(property, value); }); } return newRecord; }; } /* Helpers */ export const replaceUndefinedWithString = (val: any) => { return val === null || val === undefined || (typeof val === 'number' && isNaN(val)) ? '' : val }; /* Types */ export type QBRecordLoad = QuickBaseRequest & { query?: string; clist?: string|number[]; } export type QBRecordData = Record<any, any>; export type QBFids<T extends QBRecordData> = { [K in keyof T]: number; }; export type QBRecordOptions<RecordData extends QBRecordData = { recordid: number; primaryKey: number; }> = { quickbase: QuickBase | QuickBaseOptions; tableId: string; fids: Partial<QBFids<RecordData>>, recordid?: number; primaryKey?: number; } export type QBRecordJSON<RecordData extends QBRecordData = { recordid: number; primaryKey: number; }> = { quickbase: QuickBaseOptions; tableId: string; fids: QBFids<RecordData>; recordid: string | number; primaryKey: string | number; fields: QBFieldJSON[]; data: RecordData; } /* Export to Browser */ if(IS_BROWSER){ window.QBRecord = exports; }