UNPKG

qb-field

Version:

A lightweight abstraction layer for Quick Base

554 lines (473 loc) 12.5 kB
'use strict'; /* Dependencies */ import merge from 'deepmerge'; import RFC4122 from 'rfc4122'; import { QuickBase, QuickBaseError, QuickBaseOptions, QuickBaseRequest, QuickBaseResponseCreateField, QuickBaseResponseDeleteFields, QuickBaseResponseGetField, QuickBaseResponseGetFieldUsage, QuickBaseResponseUpdateField } from 'quickbase'; /* Globals */ const VERSION = require('../package.json').version; const IS_BROWSER = typeof(window) !== 'undefined'; const rfc4122 = new RFC4122(); const BASE_USAGE = { actions: { count: 0 }, appHomePages: { count: 0 }, dashboards: { count: 0 }, defaultReports: { count: 0 }, exactForms: { count: 0 }, fields: { count: 0 }, forms: { count: 0 }, notifications: { count: 0 }, personalReports: { count: 0 }, relationships: { count: 0 }, reminders: { count: 0 }, reports: { count: 0 }, roles: { count: 0 }, tableImports: { count: 0 }, tableRules: { count: 0 }, webhooks: { count: 0 } }; /* Main Class */ export class QBField<CustomGetSet extends Object = Record<any, any>> { public readonly CLASS_NAME = 'QBField'; static readonly CLASS_NAME = 'QBField'; static readonly VERSION: string = VERSION; /** * The default settings of a `QuickBase` instance */ static defaults: QBFieldOptions = { 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 ''; })(), fid: -1 }; /** * An internal id (guid) used for tracking/managing object instances */ public id: string; private _qb: QuickBase; private _tableId: string = ''; private _fid: number = -1; private _data: Record<any, any> = {}; private _usage: QuickBaseResponseGetFieldUsage[0]['usage'] = merge({}, BASE_USAGE); constructor(options?: Partial<QBFieldOptions>){ this.id = rfc4122.v4(); const { quickbase, ...classOptions } = options || {}; if(QuickBase.IsQuickBase(quickbase)){ this._qb = quickbase; }else{ this._qb = new QuickBase(merge.all([ QBField.defaults.quickbase, quickbase || {} ])); } const settings = merge(QBField.defaults, classOptions); this.setTableId(settings.tableId) .setFid(settings.fid); return this; } /** * This method clears the QBField instance of any trace of the existing field, but preserves defined connection settings. */ clear(): this { this._fid = -1; this._data = {}; this._usage = merge({}, BASE_USAGE); return this; } /** * This method deletes the field from QuickBase, then calls `.clear()`. */ async delete({ requestOptions }: QuickBaseRequest = {}): Promise<QuickBaseResponseDeleteFields> { const fid = this.get('id'); const fin = (deletedFieldIds: number[]) => { this.clear(); return { deletedFieldIds, errors: [] }; } if(!fid){ return fin([ fid ]); } try { const { headers, data } = await this._qb.deleteFields({ tableId: this.getTableId(), fieldIds: [ fid ], returnAxios: true, requestOptions }); if(data.errors && data.errors.length > 0){ throw new QuickBaseError(1000, 'Unable to delete field', data.errors[0], headers['qb-api-ray']); } this.clear(); return data; }catch(err: any){ if(err.description === `Field: ${fid} was not found.`){ return fin([ fid ]); } throw err; } } /** * Get an attribute value * * @param attribute Quick Base Field attribute name */ get(attribute: 'tableId'): string; get(attribute: 'fid'): number; get(attribute: 'id'): number; get(attribute: 'type'): string; get(attribute: 'addToForms'): boolean; get(attribute: 'usage'): QuickBaseResponseGetFieldUsage[0]['usage']; get<P extends keyof QuickBaseResponseGetField>(prop: P): QuickBaseResponseGetField[P]; get<P extends keyof CustomGetSet>(prop: P): CustomGetSet[P]; get<P extends string>(prop: P): P extends keyof QuickBaseResponseGetField ? QuickBaseResponseGetField[P] : (P extends keyof CustomGetSet ? CustomGetSet[P] : any); get(attribute: any): any { if(attribute === 'tableId'){ return this.getTableId(); }else if(attribute === 'fid' || attribute === 'id'){ return this.getFid(); }else if(attribute === 'usage'){ return this.getUsage(); } if(attribute === 'type'){ attribute = 'fieldType'; } return this._data[attribute]; } /** * Get the set QBField Field ID */ getFid(): number { return this._fid; } /** * Get the set QBField Table ID */ getTableId(): string { return this._tableId; } async getTempToken({ requestOptions }: QuickBaseRequest = {}): Promise<void> { await this._qb.getTempTokenDBID({ dbid: this.getTableId(), requestOptions }); } /** * Get the Quick Base Field usage * * `.loadUsage()` must be called first */ getUsage(): QuickBaseResponseGetFieldUsage[0]['usage'] { return this._usage; } /** * Load the Quick Base Field attributes and permissions */ async load({ requestOptions }: QuickBaseRequest = {}): Promise<QuickBaseResponseGetField> { const results = await this._qb.getField({ tableId: this.getTableId(), fieldId: this.getFid(), requestOptions }); Object.entries(results).forEach(([ attribute, value ]) => { this.set(attribute as keyof QuickBaseResponseGetField, value); }); return this._data as QuickBaseResponseGetField; } /** * Load the Quick Base Field usage */ async loadUsage({ requestOptions }: QuickBaseRequest = {}): Promise<QuickBaseResponseGetFieldUsage[0]['usage']> { const results = await this._qb.getFieldUsage({ tableId: this.getTableId(), fieldId: this.getFid(), requestOptions }); this.set('usage', results[0].usage); return this.getUsage(); } /** * If a field id is not defined, this will execute the createField option. After a successful * createField, or if a field id was previously defined, this will execute an updateField. * * If `attributesToSave` is defined, then only configured attributes in this array will be saved. * * If this executes a createField, the newly assigned Field ID is automatically stored internally. * * After a successful save, all new attributes are available for use. * * @param attributesToSave Array of attributes to save */ async save({ attributesToSave, requestOptions }: QuickBaseRequest & { attributesToSave?: QBFieldAttributeSavable[]; } = {}): Promise<QuickBaseResponseGetField> { const data: any = { tableId: this.getTableId(), label: this.get('label'), requestOptions }; const saveable: QBFieldAttributeSavable[] = [ 'fieldHelp', 'permissions', 'label', 'noWrap', 'bold', 'appearsByDefault', 'findEnabled' ]; if(this.getFid() > 0){ saveable.push( 'required', 'unique' ); }else{ saveable.push( 'fieldType', 'mode', 'audited', ); } // TODO // Need to filter out properties based on field type // Field schema is not "round-trip", the returned schema // has attributes in the properties object that are not // accepted when saved. For now, only save the properties // object when requested. if(attributesToSave?.indexOf('properties')){ saveable.push('properties'); } saveable.filter((attribute) => { return !attributesToSave || attributesToSave.indexOf(attribute) !== -1; }).forEach((attribute) => { data[attribute] = this.get(attribute); }); let results: QuickBaseResponseCreateField | QuickBaseResponseUpdateField; if(this.getFid() > 0){ data.fieldId = this.getFid(); results = await this._qb.updateField(data); }else{ results = await this._qb.createField(data); } Object.entries(results).forEach(([ attribute, val ]) => { this.set(attribute as keyof QuickBaseResponseUpdateField, val); }); return this._data as QuickBaseResponseUpdateField; } /** * Sets the passed in `value` associated with the `attribute` argument. * * @param attribute Quick Base Field attribute name * @param value Attribute value */ set(attribute: 'tableId', value: string): this; set(attribute: 'fid', value: number): this; set(attribute: 'id', value: number): this; set(attribute: 'type', value: string): this; set(attribute: 'addToForms', value: boolean): this; set(attribute: 'usage', value: QuickBaseResponseGetFieldUsage[0]['usage']): this; set<P extends keyof QuickBaseResponseGetField>(prop: P, value: QuickBaseResponseGetField[P]): void; set<P extends keyof CustomGetSet>(prop: P, value: CustomGetSet[P]): void; set<P extends string>(prop: P, value: P extends keyof QuickBaseResponseGetField ? QuickBaseResponseGetField[P] : (P extends keyof CustomGetSet ? CustomGetSet[P] : any)): void; set(attribute: string, value: any): this { if(attribute === 'tableId'){ return this.setTableId(value); }else if(attribute === 'fid' || attribute === 'id'){ return this.setFid(value); }else if(attribute === 'usage'){ this._usage = value; }else{ if(attribute === 'type'){ attribute = 'fieldType'; } this._data[attribute] = value; } return this; } /** * Sets the defined Field ID * * An alias for `.set('id', 6)` and `.set('fid', 6)`. * * @param fid Quick Base Field ID */ setFid(fid: number): this { this._fid = fid; return this; } /** * Sets the defined Table ID * * An alias for `.set('tableId', 'xxxxxxxxx')`. * * @param tableId Quick Base Field Table ID */ setTableId(tableId: string): this { this._tableId = tableId; return this; } /** * Rebuild the QBField instance from serialized JSON * * @param json QBField serialized JSON */ fromJSON(json: string | QBFieldJSON): 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.fid || json.id){ this.setFid(json.fid || json.id || -1); } if(json.data){ Object.entries(json.data).forEach(([ key, value ]) => { this.set(key as keyof QuickBaseResponseGetField, value); }); } if(json.usage){ this._usage = json.usage; } return this; } /** * Serialize the QBField instance into JSON */ toJSON(): QBFieldJSON { return { quickbase: this._qb.toJSON(), tableId: this.getTableId(), fid: this.getFid(), data: merge({}, this._data), usage: this.getUsage() }; } /** * Create a new QBField instance from serialized JSON * * @param json QBField serialized JSON */ static fromJSON(json: string | QBFieldJSON): QBField { 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 newField = new QBField(); return newField.fromJSON(json); } /** * Test if a variable is a `qb-field` object * * @param obj A variable you'd like to test */ static IsQBField(obj: any): obj is QBField { return ((obj || {}) as QBField).CLASS_NAME === QBField.CLASS_NAME; } /** * Returns a new QBField instance built off of `options`, that inherits configuration data from the passed in `attributes` argument. * * @param options QBField instance options * @param attributes Quick Base Field attribute data */ static NewField(options: Partial<QBFieldOptions>, attributes?: Partial<QuickBaseResponseGetField> & Record<any, any>): QBField { const newField = new QBField(options); if(attributes){ Object.entries(attributes).forEach(([ attribute, value ]) => { newField.set(attribute, value); }); } return newField; } } /* Types */ export type QBFieldAttribute = keyof QuickBaseResponseGetField; export type QBFieldAttributeSavable = Exclude<QBFieldAttribute, 'usage' | 'type' | 'doesDataCopy' | 'fid'>; export type QBFieldOptions = { quickbase: QuickBaseOptions | QuickBase; tableId: string; fid: number; } export type QBFieldJSON = { quickbase?: QuickBaseOptions; tableId: string; fid: number; id?: number; data?: Partial<QuickBaseResponseGetField> & Record<any, any>; usage?: QuickBaseResponseGetFieldUsage[0]['usage']; } /* Export to Browser */ if(IS_BROWSER){ window.QBField = exports; }