@jd-data-limited/easy-fm
Version:
easy-fm is a Node.js module that allows you to interact with a [FileMaker database stored](https://www.claris.com/filemaker/) on a [FileMaker server](https://www.claris.com/filemaker/server/). This module interacts with your server using the [FileMaker
234 lines (233 loc) • 9.25 kB
JavaScript
/*
* Copyright (c) 2023-2024. See LICENSE file for more information
*/
import { LayoutRecord } from '../layoutRecord.js';
import { FMError } from '../../FMError.js';
import { FindRequestSymbol } from '../../utils/query.js';
export class RecordGetOperation {
layout;
limit = 100;
scriptData = {};
sortData = [];
portals;
offset = 1;
requests = [];
constructor(layout, options) {
this.layout = layout;
this.sortData = [];
this.portals = options.portals;
this.offset = options.offset ?? 1; // Offset refers to the starting record. offset 1 is the same as no offset.
this.limit = options.limit ?? 100;
if (options.requests) {
for (const req of options.requests)
this.addRequest(req.req, req.omit ?? false);
}
}
get isFindRequest() {
return this.requests.length !== 0;
}
formatQueries() {
const test = this.requests.map(query => {
const out = {};
for (const key of Object.keys(query.req)) {
if (query.req[key])
out[key] = query.req[key];
else {
out[key] = query.req[key];
}
}
if (query.omit)
out.omit = 'true';
return out;
});
return test;
}
generateParamsBody(offset, limit) {
const params = {
limit: limit.toString(),
offset: offset.toString(),
dateformats: 2 // Ensure dates are received in ISO8601 format
};
if (this.sortData.length !== 0)
params.sort = this.sortData;
if (this.scriptData.after)
params.script = this.scriptData.after.name;
if (this.scriptData.after?.parameter)
params['script.param'] = this.scriptData.after.parameter;
if (this.scriptData.presort)
params['script.presort'] = this.scriptData.presort.name;
if (this.scriptData.presort?.parameter)
params['script.presort.param'] = this.scriptData.presort.parameter;
if (this.scriptData.prerequest)
params['script.prerequest'] = this.scriptData.prerequest.name;
if (this.scriptData.prerequest?.parameter)
params['script.prerequest.param'] = this.scriptData.prerequest.parameter;
if (this.requests.length !== 0)
params.query = this.formatQueries();
const portals = Object.keys(this.portals);
params.portal = portals;
for (const portal of portals) {
params[`offset.${portal.toString()}`] = this.portals[portal]?.offset;
params[`limit.${portal.toString()}`] = this.portals[portal]?.limit;
}
return params;
}
generateParamsURL(offset, limit) {
const params = new URLSearchParams({
_limit: limit.toString(),
_offset: offset.toString(),
dateformats: '2' // Ensure dates are received in ISO8601 format
});
if (this.sortData.length !== 0)
params.set('_sort', JSON.stringify(this.sortData));
if (this.scriptData.after)
params.set('script', this.scriptData.after.name);
if (this.scriptData.after?.parameter)
params.set('script.param', this.scriptData.after.parameter);
if (this.scriptData.presort)
params.set('script.presort', this.scriptData.presort.name);
if (this.scriptData.presort?.parameter)
params.set('script.presort.param', this.scriptData.presort.parameter);
if (this.scriptData.prerequest)
params.set('script.prerequest', this.scriptData.prerequest.name);
if (this.scriptData.prerequest?.parameter)
params.set('script.prerequest.param', this.scriptData.prerequest.parameter);
const portals = Object.keys(this.portals);
for (const portal of portals) {
params.set(`_offset.${portal.toString()}`, (this.portals[portal]?.limit ?? '').toString());
params.set(`_offset.${portal.toString()}`, (this.portals[portal]?.offset ?? '').toString());
}
params.set('portal', JSON.stringify(portals));
return params;
}
/**
* Configures any FileMaker scripts to be run as a part of the request
*
* @param {ScriptRequestData} scripts - The script request data to set.
* @return {this} - The current instance of the class.
*/
scripts(scripts) {
this.scriptData = scripts;
return this;
}
/**
* Sorts the data based on the given field name and sort order.
*
* @param {string} fieldName - The name of the field by which the data should be sorted.
* @param {SortOrder} sortOrder - The sort order to be applied (either "asc" for ascending or "desc" for descending).
*
* @return {this} - Returns the current instance of the object.
*/
sort(fieldName, sortOrder) {
this.sortData.push({ fieldName, sortOrder });
return this;
}
parseFindRequest(query) {
const out = {};
for (const key of Object.keys(query)) {
out[key] = query[key][FindRequestSymbol].map(item => {
if (typeof item === 'string')
return item;
// Re-write date into correct format
return item
.moment
.clone()
.utcOffset(this.layout.database.host.timezoneOffsetFunc(item.moment))
.format(item.type === 'date'
? this.layout.database.host.dateFormat
: item.type == 'time'
? this.layout.database.host.timeFormat
: this.layout.database.host.timeStampFormat);
}).join('');
}
return out;
}
/**
* Adds a new request/query to the list of queries.
*
* @param {FindRequest} query - The find request to be added.
* @param {boolean} [omit=false] - Flag to indicate if the find request should be omitted.
* @return {Object} - The current object instance.
*/
addRequest(query, omit = false) {
this.requests.push({ req: this.parseFindRequest(query), omit });
return this;
}
/**
* Perform a fetch operation.
*
* @returns {Promise} A promise that resolves with the result of the fetch operation.
*/
async fetch() {
return await this.performFind(this.offset, this.limit);
}
async performFind(offset, limit) {
const trace = new Error();
await this.layout.getLayoutMeta();
const isFind = this.isFindRequest;
let endpoint = this.layout.endpoint + (isFind ? '/_find' : '/records');
if (!isFind)
endpoint += '?' + new URLSearchParams(this.generateParamsURL(offset, limit)).toString();
const reqData = {
// port: 443,
method: isFind ? 'POST' : 'GET',
body: isFind ? JSON.stringify(this.generateParamsBody(offset, limit)) : undefined
};
try {
const res = await this.layout.database._apiRequestJSON(endpoint, reqData);
if (res.messages[0].code === '0' && res.response) {
// console.log("RESOLVING")
if (!this.layout.metadata)
await this.layout.getLayoutMeta();
return res.response.data.map(item => {
return new LayoutRecord(this.layout, item.recordId, item.modId, item.fieldData, item.portalData);
});
}
else {
throw new FMError(res.messages[0].code, res.httpStatus, res, trace);
}
}
catch (e) {
if (e instanceof FMError) {
if (e.code === 401) {
// No records found, so return empty set
return [];
}
}
throw e;
}
}
[Symbol.asyncIterator]() {
let nextOffset = this.offset;
const startOffset = JSON.parse(JSON.stringify(this.offset));
const limit = this.limit;
let exitAfterLastRecord = false;
let records = [];
const fetch = async () => {
const theoreticalLimit = (limit - nextOffset) + startOffset;
if (theoreticalLimit === 0) {
exitAfterLastRecord = true;
records = [];
return;
}
records = await this.performFind(nextOffset, theoreticalLimit < 100 ? theoreticalLimit : 100);
nextOffset += 100;
if (records.length < 100)
exitAfterLastRecord = true;
};
return {
next: async () => {
if (records.length === 0 && !exitAfterLastRecord) {
await fetch();
}
if (records.length === 0 && exitAfterLastRecord) {
return { done: true, value: undefined };
}
else {
const record = records.shift();
return { done: false, value: record };
}
}
};
}
}