@plattar/sdk-core
Advanced tools
Comparing version
@@ -0,1 +1,2 @@ | ||
import { CoreObjectRelations } from "./relations/core-object-relations"; | ||
/** | ||
@@ -6,3 +7,26 @@ * This interface will need to be implemented by the SDK generator | ||
} | ||
export interface CoreObjectPayload { | ||
readonly data: { | ||
readonly attributes: CoreObjectAttributes; | ||
}; | ||
} | ||
/** | ||
* This is input from the fetch operation with the required data to construct this object and all | ||
* internal hierarcies | ||
* | ||
* data - the primary data that belongs to this object | ||
* records - a global list of additional records that might belong to this object (will be filtered) | ||
* cache - a global cache map to break recursion so multiple object of same type are not created | ||
*/ | ||
export interface FetchData { | ||
readonly object: { | ||
readonly id: string; | ||
readonly type: string; | ||
readonly attributes: any; | ||
readonly relationships: any; | ||
}; | ||
readonly includes: Map<string, any>; | ||
readonly cache: Map<string, CoreObject<CoreObjectAttributes>>; | ||
} | ||
/** | ||
* CoreObject is the base object that all Objects in the API derive base functionality from | ||
@@ -12,5 +36,11 @@ */ | ||
private readonly _attributes; | ||
private readonly _relations; | ||
private _id; | ||
constructor(id?: string | null, attributes?: Attributes); | ||
get attributes(): Attributes; | ||
get relationships(): CoreObjectRelations; | ||
/** | ||
* Generates a JSON Payload that can be sent to a backend server | ||
*/ | ||
get payload(): CoreObjectPayload; | ||
get id(): string; | ||
@@ -27,4 +57,12 @@ hasID(): boolean; | ||
* Re-fills tis object instance with data from the api | ||
* | ||
* data - the primary data that belongs to this object | ||
* records - a global list of additional records that might belong to this object (will be filtered) | ||
* cache - a global cache map to break recursion so multiple object of same type are not created | ||
*/ | ||
setFromAPI(data: any): void; | ||
setFromAPI(data: FetchData): void; | ||
/** | ||
* internal use function by setFromAPI that constructs a new record | ||
*/ | ||
private _CreateRecord; | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.CoreObject = void 0; | ||
const global_object_pool_1 = require("./global-object-pool"); | ||
const core_object_relations_1 = require("./relations/core-object-relations"); | ||
/** | ||
@@ -10,2 +12,4 @@ * CoreObject is the base object that all Objects in the API derive base functionality from | ||
_attributes; | ||
// these are a list of all objects related to this object | ||
_relations; | ||
// every object has a unique ID assigned, this is filled by the remote API | ||
@@ -16,2 +20,3 @@ _id; | ||
this._attributes = attributes ? attributes : {}; | ||
this._relations = new core_object_relations_1.CoreObjectRelations(this); | ||
} | ||
@@ -21,2 +26,15 @@ get attributes() { | ||
} | ||
get relationships() { | ||
return this._relations; | ||
} | ||
/** | ||
* Generates a JSON Payload that can be sent to a backend server | ||
*/ | ||
get payload() { | ||
return { | ||
data: { | ||
attributes: this.attributes | ||
} | ||
}; | ||
} | ||
get id() { | ||
@@ -65,18 +83,75 @@ if (!this._id) { | ||
* Re-fills tis object instance with data from the api | ||
* | ||
* data - the primary data that belongs to this object | ||
* records - a global list of additional records that might belong to this object (will be filtered) | ||
* cache - a global cache map to break recursion so multiple object of same type are not created | ||
*/ | ||
setFromAPI(data) { | ||
// error out if we try to write the data from the api into the wrong type | ||
if (this.type !== data.type) { | ||
throw new Error(`CoreObject.setFromAPI() - type mismatch, cannot set ${this.type} from data type ${data.type}`); | ||
if (this.type !== data.object.type) { | ||
throw new Error(`CoreObject.setFromAPI() - type mismatch, cannot set ${this.type} from data type ${data.object.type}`); | ||
} | ||
// assign the ID | ||
this._id = data.id; | ||
// clear all previous cache as new object is getting constructed | ||
this.relationships.cache.clear(); | ||
// assign the ID from the record | ||
this._id = data.object.id; | ||
// delete all previous keys from our object instance | ||
Object.keys(this._attributes).forEach(key => delete (this._attributes)[key]); | ||
// assign new keys to our attributes | ||
for (const [key, value] of Object.entries(data.attributes)) { | ||
// NOTE: this could probably be optimized by using attributes directly instead of deep-copy | ||
for (const [key, value] of Object.entries(data.object.attributes)) { | ||
(this._attributes)[key] = value; | ||
} | ||
// we need to build the relationships of this object from the records section | ||
// which includes all the records from any include query | ||
for (const [_key, value] of Object.entries(data.object.relationships)) { | ||
const relationRecord = value.data; | ||
// check if the object exists in the includes section - the value | ||
// can either be a single object or an array | ||
// this only contains id or type but not the full record | ||
if (Array.isArray(relationRecord)) { | ||
const arrayRecord = relationRecord; | ||
arrayRecord.forEach((record) => { | ||
this._CreateRecord(data, record); | ||
}); | ||
} | ||
else { | ||
this._CreateRecord(data, relationRecord); | ||
} | ||
} | ||
} | ||
/** | ||
* internal use function by setFromAPI that constructs a new record | ||
*/ | ||
_CreateRecord(data, record) { | ||
const includedRecord = data.includes.get(record.id); | ||
// quick exit - we don't need to do anything if record doesn't exist | ||
// and doesn't want to be constructed | ||
if (!includedRecord) { | ||
return; | ||
} | ||
// check the cache to see if this record was previously constructed | ||
// if so, we use that and quick exit | ||
const cachedRecord = data.cache.get(record.id); | ||
if (cachedRecord) { | ||
this.relationships.cache.append(cachedRecord); | ||
return; | ||
} | ||
// otherwise, create a new record and add it as a relation | ||
const newObject = global_object_pool_1.GlobalObjectPool.newInstance(record.type); | ||
if (!newObject) { | ||
throw new Error(`record constructor is unable to create a new record of type ${record.type}`); | ||
} | ||
// add the new object into the cache | ||
data.cache.set(record.id, newObject); | ||
// recursively construct the new object | ||
newObject.setFromAPI({ | ||
object: record, | ||
includes: data.includes, | ||
cache: data.cache | ||
}); | ||
// add as a relationship to the current object | ||
this.relationships.cache.append(newObject); | ||
} | ||
} | ||
exports.CoreObject = CoreObject; |
@@ -18,6 +18,15 @@ import { CoreObject, CoreObjectAttributes } from '../core-object'; | ||
private readonly _queries; | ||
private readonly _abort; | ||
constructor(instance: T, service?: Service); | ||
get instance(): T; | ||
get service(): Service; | ||
where(variable: keyof U, operation: FilterQueryOperator | SearchQueryOperator, value: string | number | boolean): this; | ||
/** | ||
* Call this to abort/terminate incomplete query requests | ||
*/ | ||
abort(reason?: string): void; | ||
/** | ||
* Allows multiple queries to be joined together | ||
*/ | ||
join(...queries: Array<CoreQuery<CoreObject<CoreObjectAttributes>, CoreObjectAttributes>>): this; | ||
where(variable: keyof U, operation: FilterQueryOperator | SearchQueryOperator, value: string | number | boolean | Date): this; | ||
fields(...fields: Array<keyof U>): this; | ||
@@ -29,2 +38,5 @@ include(...objects: Array<(typeof CoreObject<CoreObjectAttributes>) | Array<string>>): this; | ||
page(count: number, size: number): this; | ||
/** | ||
* Performs the primary request and returns the responses as an array | ||
*/ | ||
protected _Fetch(url: string, type: QueryFetchType): Promise<Array<T>>; | ||
@@ -35,2 +47,3 @@ /** | ||
toString(): string; | ||
static fetch<T extends CoreObject<CoreObjectAttributes>>(service: Service, instance: T, encodedURL: string, type: QueryFetchType, abort?: AbortSignal): Promise<Array<T>>; | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.CoreQuery = void 0; | ||
const global_object_pool_1 = require("../global-object-pool"); | ||
const service_1 = require("../service"); | ||
const core_error_1 = require("./errors/core-error"); | ||
const contains_query_1 = require("./queries/contains-query"); | ||
@@ -10,2 +12,3 @@ const deleted_query_1 = require("./queries/deleted-query"); | ||
const include_query_1 = require("./queries/include-query"); | ||
const join_query_1 = require("./queries/join-query"); | ||
const pagination_query_1 = require("./queries/pagination-query"); | ||
@@ -21,2 +24,3 @@ const search_query_1 = require("./queries/search-query"); | ||
_queries; | ||
_abort; | ||
constructor(instance, service) { | ||
@@ -26,2 +30,3 @@ this._instance = instance; | ||
this._queries = new Array(); | ||
this._abort = new AbortController(); | ||
} | ||
@@ -34,2 +39,17 @@ get instance() { | ||
} | ||
/** | ||
* Call this to abort/terminate incomplete query requests | ||
*/ | ||
abort(reason) { | ||
this._abort.abort(reason); | ||
} | ||
/** | ||
* Allows multiple queries to be joined together | ||
*/ | ||
join(...queries) { | ||
queries.forEach((query) => { | ||
this._queries.push(new join_query_1.JoinQuery(query.toString())); | ||
}); | ||
return this; | ||
} | ||
where(variable, operation, value) { | ||
@@ -39,6 +59,6 @@ switch (operation) { | ||
case '~=': | ||
this._queries.push(new search_query_1.SearchQuery(this.instance.type, variable, value)); | ||
this._queries.push(new search_query_1.SearchQuery(this.instance.type, variable, (value instanceof Date ? value.toISOString() : value))); | ||
break; | ||
default: | ||
this._queries.push(new filter_query_1.FilterQuery(this.instance.type, variable, operation, value)); | ||
this._queries.push(new filter_query_1.FilterQuery(this.instance.type, variable, operation, (value instanceof Date ? value.toISOString() : value))); | ||
break; | ||
@@ -92,11 +112,7 @@ } | ||
} | ||
/** | ||
* Performs the primary request and returns the responses as an array | ||
*/ | ||
async _Fetch(url, type) { | ||
const results = new Array(); | ||
// encode the full url to safely escape all characters (like whitespaces) | ||
const encodedURL = encodeURI(url + this.toString()); | ||
// proceed with generating the request - for anything other than GET we need to generate a payload | ||
// this payload is generated from non-null values of the object attributes | ||
// TO-DO | ||
// return the final results which might contain 0 or more objects (depending on the request) | ||
return results; | ||
return CoreQuery.fetch(this.service, this.instance, encodeURI(`${url}?${this.toString()}`), type, this._abort.signal); | ||
} | ||
@@ -111,3 +127,3 @@ /** | ||
} | ||
let url = '?'; | ||
let url = ''; | ||
queries.forEach((query) => { | ||
@@ -119,3 +135,196 @@ url += `${query.toString()}&`; | ||
} | ||
static async fetch(service, instance, encodedURL, type, abort) { | ||
const results = new Array(); | ||
// init our request type | ||
const request = { | ||
method: type, | ||
mode: 'cors', | ||
cache: 'no-cache', | ||
credentials: 'include', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'Accept': 'application/json' | ||
}, | ||
redirect: 'follow', | ||
referrerPolicy: 'origin' | ||
}; | ||
// send the payload if performing POST/PUT/PATCH requests | ||
switch (type) { | ||
case 'POST': | ||
case 'PUT': | ||
case 'PATCH': | ||
request.body = JSON.stringify(instance.payload); | ||
break; | ||
} | ||
// set the abort signal to terminate the query if needed | ||
if (abort) { | ||
request.signal = abort; | ||
} | ||
// proceed with generating the request - for anything other than GET we need to generate a payload | ||
// this payload is generated from non-null values of the object attributes | ||
try { | ||
const response = await fetch(encodedURL, request); | ||
if (!response.ok) { | ||
core_error_1.CoreError.init({ | ||
error: { | ||
title: 'Network Error', | ||
text: `there was an unexpected issue with the network` | ||
} | ||
}).handle(service); | ||
return results; | ||
} | ||
// catch backend timeout errors for long-running requests | ||
if (response.status === 408) { | ||
core_error_1.CoreError.init({ | ||
error: { | ||
status: 408, | ||
title: 'Request Timeout', | ||
text: `request timed out` | ||
} | ||
}).handle(service); | ||
return results; | ||
} | ||
let json = null; | ||
try { | ||
json = await response.json(); | ||
} | ||
catch (err) { | ||
core_error_1.CoreError.init({ | ||
error: { | ||
title: 'Runtime Error', | ||
text: `something unexpected occured during results parsing, details - ${err.message}` | ||
} | ||
}).handle(service); | ||
return results; | ||
} | ||
// ensure there is a json object that was parsed properly | ||
if (!json) { | ||
core_error_1.CoreError.init({ | ||
error: { | ||
title: 'Runtime Error', | ||
text: 'runtime expected json results from fetch to be non-null' | ||
} | ||
}).handle(service); | ||
return results; | ||
} | ||
// check if the returned data is json error object | ||
if (json.error) { | ||
core_error_1.CoreError.init(json).handle(service); | ||
return results; | ||
} | ||
// ensure json has the critical data section in-tact | ||
if (!json.data) { | ||
core_error_1.CoreError.init({ | ||
error: { | ||
title: 'Runtime Error', | ||
text: 'runtime tried to parse malformed json data' | ||
} | ||
}).handle(service); | ||
return results; | ||
} | ||
// map includes query into a map structure | ||
const includes = json.included || new Array(); | ||
const includesMap = new Map(); | ||
// fill in the includes map for faster Lookup when creating object hierarchies | ||
includes.forEach((includesRecord) => { | ||
if (includesRecord.id) { | ||
includesMap.set(includesRecord.id, includesRecord); | ||
} | ||
}); | ||
// begin parsing the json, which should be the details of the current | ||
// object type - this could also be an array so we'll need extra object instances | ||
// if Array - we are dealing with multiple records, otherwise its a single record | ||
if (Array.isArray(json.data)) { | ||
const listRecords = json.data; | ||
// we don't have ANY results, return an empty array | ||
if (listRecords.length <= 0) { | ||
return results; | ||
} | ||
// otherwise, the first result will be our current instance and any | ||
// consecutive results will be created dynamically | ||
// we create a global LUT cache to keep track of recursions | ||
const cache = new Map(); | ||
const object = listRecords[0]; | ||
// add the first object to the instance | ||
cache.set(object.id, instance); | ||
// construct the first object | ||
instance.setFromAPI({ | ||
object: object, | ||
includes: includesMap, | ||
cache: cache | ||
}); | ||
results.push(instance); | ||
// begin construction of every other instance | ||
for (let i = 1; i < listRecords.length; i++) { | ||
const record = listRecords[i]; | ||
const objectInstance = cache.get(object.id) || global_object_pool_1.GlobalObjectPool.newInstance(record.type); | ||
if (!objectInstance) { | ||
core_error_1.CoreError.init({ | ||
error: { | ||
title: 'Runtime Error', | ||
text: `runtime could not create a new instance of object type ${record.type} at index ${i}` | ||
} | ||
}).handle(service); | ||
continue; | ||
} | ||
// add the first object to the instance | ||
cache.set(record.id, objectInstance); | ||
objectInstance.setFromAPI({ | ||
object: listRecords[i], | ||
includes: includesMap, | ||
cache: cache | ||
}); | ||
results.push(objectInstance); | ||
} | ||
} | ||
else { | ||
// handle single record types | ||
const record = json.data; | ||
// we don't have ANY results, return an empty array | ||
if (!record.type || !record.id) { | ||
return results; | ||
} | ||
// otherwise, the first result will be our current instance and any | ||
// consecutive results will be created dynamically | ||
// we create a global LUT cache to keep track of recursions | ||
const cache = new Map(); | ||
// add the first object to the instance | ||
cache.set(record.id, instance); | ||
// construct the first object | ||
instance.setFromAPI({ | ||
object: record, | ||
includes: includesMap, | ||
cache: cache | ||
}); | ||
results.push(instance); | ||
} | ||
} | ||
catch (err) { | ||
// throw the signal error in case the request was canelled | ||
if (abort && abort.aborted) { | ||
core_error_1.CoreError.init({ | ||
error: { | ||
title: 'Aborted', | ||
text: 'request was manually aborted' | ||
} | ||
}).handle(service); | ||
return results; | ||
} | ||
// throw general errors | ||
if (err instanceof core_error_1.CoreError) { | ||
err.handle(service); | ||
} | ||
else { | ||
core_error_1.CoreError.init({ | ||
error: { | ||
title: 'Runtime Error', | ||
text: `something unexpected occured during runtime, details - ${err.message}` | ||
} | ||
}).handle(service); | ||
} | ||
} | ||
// return the final results which might contain 0 or more objects (depending on the request) | ||
return results; | ||
} | ||
} | ||
exports.CoreQuery = CoreQuery; |
@@ -13,3 +13,3 @@ export type ServiceAuthType = 'cookie' | 'token'; | ||
readonly errorListener?: ServiceErrorListener | null; | ||
readonly version?: string | null; | ||
readonly version?: number | null; | ||
} | ||
@@ -22,10 +22,2 @@ export interface ServiceConfig { | ||
/** | ||
* Static container used for holding the default service, since sdk-core is used | ||
* in multiple projects, using an anti-pattern can become troublesome for state | ||
* storage | ||
*/ | ||
export interface ServiceStaticContainer { | ||
service: Service | null; | ||
} | ||
/** | ||
* Locked down, immutable version of ServiceConfig with defaults already set | ||
@@ -50,3 +42,4 @@ */ | ||
*/ | ||
export declare abstract class Service { | ||
export declare class Service { | ||
private static _defaultServiceInstance; | ||
private readonly _config; | ||
@@ -57,3 +50,3 @@ constructor(config: ServiceConfig); | ||
*/ | ||
static config(_config: ServiceConfig): Service; | ||
static config(config: ServiceConfig): Service; | ||
/** | ||
@@ -63,3 +56,2 @@ * Returns the default service object | ||
static get default(): Service; | ||
static get container(): ServiceStaticContainer; | ||
/** | ||
@@ -66,0 +58,0 @@ * Returns the currently locked, read-only Service Configuration |
@@ -9,2 +9,3 @@ "use strict"; | ||
class Service { | ||
static _defaultServiceInstance; | ||
_config; | ||
@@ -17,3 +18,3 @@ constructor(config) { | ||
options: { | ||
version: (config.options && config.options.version) ? config.options.version : 'v3', | ||
version: (config.options && config.options.version) ? `v${config.options.version}` : 'v3', | ||
tls: (config.options && config.options.tls) ? util_1.Util.parseBool(config.options.tls) : false, | ||
@@ -42,4 +43,5 @@ gzip: (config.options && config.options.gzip) ? util_1.Util.parseBool(config.options.gzip) : false, | ||
*/ | ||
static config(_config) { | ||
throw new Error('Service.config is not implemented correctly, contact admin'); | ||
static config(config) { | ||
Service._defaultServiceInstance = new Service(config); | ||
return Service._defaultServiceInstance; | ||
} | ||
@@ -50,10 +52,7 @@ /** | ||
static get default() { | ||
if (!this.container.service) { | ||
if (!Service._defaultServiceInstance) { | ||
throw new Error('Service.default is not configured, use Service.config() to set a new default'); | ||
} | ||
return this.container.service; | ||
return Service._defaultServiceInstance; | ||
} | ||
static get container() { | ||
throw new Error('Service.container is not implemented correctly, contact admin'); | ||
} | ||
/** | ||
@@ -60,0 +59,0 @@ * Returns the currently locked, read-only Service Configuration |
import { CoreController } from "@plattar/api-core"; | ||
import { PackageJsonVars } from "./generators/project"; | ||
import { GeneratedSchema } from "./generators/schema"; | ||
export interface GeneratorData { | ||
@@ -12,3 +11,2 @@ readonly controllers: Array<typeof CoreController>; | ||
private static generateIndexFile; | ||
static generateServiceFile(data: GeneratorData): GeneratedSchema; | ||
} |
@@ -22,3 +22,2 @@ "use strict"; | ||
fs_1.default.mkdirSync(`${outputDir}/src/schemas`, { recursive: true }); | ||
fs_1.default.mkdirSync(`${outputDir}/src/core`, { recursive: true }); | ||
// write the .npmignore file | ||
@@ -39,5 +38,2 @@ await fs_1.default.promises.writeFile(`${outputDir}/${project.npmIgnore.fname}`, project.npmIgnore.data); | ||
}); | ||
const serviceSchema = this.generateServiceFile(data); | ||
// write the service file | ||
allSchemas.push(fs_1.default.promises.writeFile(`${outputDir}/src/core/${serviceSchema.fname}`, serviceSchema.data)); | ||
await Promise.all(allSchemas); | ||
@@ -49,6 +45,2 @@ // write the index.ts file | ||
schemas: schemas | ||
}, | ||
{ | ||
dir: 'core', | ||
schemas: [serviceSchema] | ||
} | ||
@@ -59,3 +51,3 @@ ])); | ||
let output = '/*\n * Warning: Do Not Edit - Auto Generated via @plattar/sdk-core\n */\n\n'; | ||
output += `export { Service, ServiceConfig, ServiceAuth, ServiceOptions, ServiceAuthType, ServiceErrorHandler, ServiceErrorListener } from '@plattar/sdk-core';\n`; | ||
output += `export { Service, ServiceConfig, ServiceAuth, ServiceOptions, ServiceAuthType, ServiceErrorHandler, ServiceErrorListener, CoreError } from '@plattar/sdk-core';\n`; | ||
files.forEach((schemas) => { | ||
@@ -68,22 +60,3 @@ schemas.schemas.forEach((schema) => { | ||
} | ||
static generateServiceFile(data) { | ||
const className = `Connection`; | ||
let output = `import { Service, ServiceConfig, ServiceStaticContainer } from '@plattar/sdk-core';\n\n`; | ||
output += `export class ${className} extends Service {\n`; | ||
output += `\tprivate static readonly serviceContainer: ServiceStaticContainer = {service:null}\n`; | ||
output += `\tpublic static override get container(): ServiceStaticContainer {\n`; | ||
output += `\t\treturn this.serviceContainer;\n`; | ||
output += `\t}\n`; | ||
output += `\tpublic static override config(config: ServiceConfig): ${className} {\n`; | ||
output += `\t\tthis.container.service = new ${className}(config);\n`; | ||
output += `\t\treturn <${className}>this.container.service;\n`; | ||
output += `\t}\n`; | ||
output += '}\n'; | ||
return { | ||
name: `connection`, | ||
fname: `connection.ts`, | ||
data: output | ||
}; | ||
} | ||
} | ||
exports.Generator = Generator; |
@@ -6,2 +6,3 @@ export * from "./core/service"; | ||
export * from "./core/query/core-query"; | ||
export * from "./core/query/errors/core-error"; | ||
export * from "./generator/generator"; |
@@ -23,3 +23,4 @@ "use strict"; | ||
__exportStar(require("./core/query/core-query"), exports); | ||
__exportStar(require("./core/query/errors/core-error"), exports); | ||
// export generator - this only works in NodeJS | ||
__exportStar(require("./generator/generator"), exports); |
@@ -1,2 +0,2 @@ | ||
declare const _default: "1.163.10"; | ||
declare const _default: "1.164.1"; | ||
export default _default; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.default = "1.163.10"; | ||
exports.default = "1.164.1"; |
{ | ||
"name": "@plattar/sdk-core", | ||
"version": "1.163.10", | ||
"version": "1.164.1", | ||
"description": "Core SDK Module for Generative SDK using API Core", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
73777
38.68%49
13.95%1879
41.07%4
33.33%