Skip to content

Commit

Permalink
Merge pull request #5 from Plattar/feature/enh-issue-2
Browse files Browse the repository at this point in the history
Initial Implementation
  • Loading branch information
DavidArayan authored Dec 13, 2023
2 parents 52e1de3 + 7215778 commit 2149852
Show file tree
Hide file tree
Showing 33 changed files with 1,951 additions and 9 deletions.
8 changes: 5 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ jobs:
run: rm -rf sdk-core/src/version.ts && echo 'export default "${{ env.RELEASE_VERSION }}";' > sdk-core/src/version.ts
- name: copy README
run: cp README.md sdk-core/README.md
- name: NPM Build
run: npm run --prefix sdk-core clean:build
- name: copy graphics
run: cp -R graphics sdk-core/
- name: NPM Login & Build
run: rm -rf sdk-core/.npmrc && npm set "//registry.npmjs.org/:_authToken" ${{ secrets.NPM_PUBLISH_KEY }} && npm run --prefix sdk-core clean:build
- uses: JS-DevTools/npm-publish@v1
with:
package: ./sdk-core/package.json
token: ${{ secrets.NPM_PUBLISH_KEY }}
access: 'restricted'
access: 'public'
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.DS_Store
sdk-core/node_modules/
sdk-core/dist/
sdk-core/generated/
sdk-core/package-lock.json
sdk-core/graph.svg
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,15 @@
# api-sdk
Typescript SDK for interfacing with the Plattar V3 API
<h3 align="center">
<img src="graphics/logo.png?raw=true" alt="Plattar Logo" width="600">
</h3>

[![NPM](https://img.shields.io/npm/v/@plattar/sdk-core)](https://www.npmjs.com/package/@plattar/sdk-core)

_sdk-core_ is a generative & runtime support module for automatically generating a TypeScript SDK to interface with Plattar backend services

### Installation

- Install using [npm](https://www.npmjs.com/package/@plattar/sdk-core)

```console
npm install @plattar/sdk-core
```
1 change: 0 additions & 1 deletion api-core/src/index.ts

This file was deleted.

Binary file added graphics/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion api-core/.npmignore → sdk-core/.npmignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
src/
node_modules/
tsconfig.json
package-lock.json
package-lock.json
.npmrc
1 change: 1 addition & 0 deletions sdk-core/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
24 changes: 22 additions & 2 deletions api-core/package.json → sdk-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,25 @@
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./runtime": {
"require": "./dist/index.js",
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./generator": {
"require": "./dist/generator/index.js",
"import": "./dist/generator/index.js",
"types": "./dist/generator/index.d.ts"
}
},
"scripts": {
"clean": "rm -rf dist node_modules package-lock.json && npm cache clean --force",
"clean": "rm -rf dist node_modules package-lock.json generated && npm cache clean --force",
"build": "npm install && npm run build-ts",
"build-ts": "tsc --noEmitOnError",
"clean:build": "npm run clean && npm run build",
Expand All @@ -21,16 +38,19 @@
"node": ">=18.0"
},
"author": "plattar",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/Plattar/sdk-core/issues"
},
"homepage": "https://www.plattar.com",
"dependencies": {},
"devDependencies": {
"typescript": "^5.2.2",
"@plattar/api-core": "^1.163.5",
"@types/node": "^18.16.0",
"madge": "^6.1.0"
},
"publishConfig": {
"access": "restricted"
"access": "public"
}
}
220 changes: 220 additions & 0 deletions sdk-core/src/core/core-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { GlobalObjectPool } from "./global-object-pool";
import { CoreObjectRelations } from "./relations/core-object-relations";

/**
* This interface will need to be implemented by the SDK generator
*/
export interface CoreObjectAttributes { }

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
*/
export abstract class CoreObject<Attributes extends CoreObjectAttributes> {

// these attributes are filled from the remote API when a query is made
private readonly _attributes: Attributes;

// these are a list of all objects related to this object
private readonly _relations: CoreObjectRelations;

// every object has a unique ID assigned, this is filled by the remote API
private _id: string | null;

public constructor(id?: string | null, attributes?: Attributes) {
this._id = id ? id : null;
this._attributes = attributes ? attributes : <Attributes>{}
this._relations = new CoreObjectRelations(this);
}

public get attributes(): Attributes {
return this._attributes;
}

public get relationships(): CoreObjectRelations {
return this._relations;
}

/**
* Generates a JSON Payload that can be sent to a backend server
*/
public get payload(): CoreObjectPayload {
return {
data: {
attributes: this.attributes
}
}
}

public get id(): string {
if (!this._id) {
throw new Error('CoreObject.id is not configured, use constructor with a non-null id');
}

return this._id;
}

public hasID(): boolean {
return this._id ? true : false;
}

public static get type(): string {
throw new Error('CoreObject.type is not implemented, contact admin');
}

public static newInstance<T extends CoreObject<CoreObjectAttributes>>(): T {
return <T>(new (<any>this)());
}

public get type(): string {
return (<any>this.constructor).type;
}

/**
* shortcut for easier chained construction of Include Queries
*/
public static include(...objects: Array<(typeof CoreObject<CoreObjectAttributes>) | Array<string>>): Array<string> {
const data: Array<string | Array<string>> = objects.map<string | Array<string>>((object: typeof CoreObject<CoreObjectAttributes> | Array<string>) => {
if (Array.isArray(object)) {
return object.map<string>((object: string) => {
return `${this.type}.${object}`;
});
}

return `${this.type}.${object.type}`;
});

const consolidatedData: Array<string> = new Array<string>();

data.forEach((object: string | Array<string>) => {
if (Array.isArray(object)) {
consolidatedData.push(...object);
}
else {
consolidatedData.push(object);
}
});

return consolidatedData;
}

/**
* 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
*/
public setFromAPI(data: FetchData) {
// error out if we try to write the data from the api into the wrong type
if (this.type !== data.object.type) {
throw new Error(`CoreObject.setFromAPI() - type mismatch, cannot set ${this.type} from data type ${data.object.type}`);
}

// 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 (<any>(this._attributes))[key]);

// assign new keys to our attributes
// NOTE: this could probably be optimized by using attributes directly instead of deep-copy
if (data.object.attributes) {
for (const [key, value] of Object.entries(data.object.attributes)) {
(<any>(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
if (data.object.relationships) {
for (const [_key, value] of Object.entries(data.object.relationships)) {
const relationRecord: any = (<any>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: Array<any> = relationRecord;

arrayRecord.forEach((record: any) => {
this._CreateRecord(data, record);
});
}
else {
this._CreateRecord(data, relationRecord);
}
}
}
}

/**
* internal use function by setFromAPI that constructs a new record
*/
private _CreateRecord(data: FetchData, record: any): void {
const includedRecord: any = 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: CoreObject<CoreObjectAttributes> | undefined = 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: CoreObject<CoreObjectAttributes> | null = 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);
}
}
36 changes: 36 additions & 0 deletions sdk-core/src/core/global-object-pool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { CoreObjectAttributes, type CoreObject } from './core-object';

/**
* This object provides runtime functionality to access object types for construction
* from the API
*/
export class GlobalObjectPool {
private static readonly _globalMap: Map<string, typeof CoreObject<CoreObjectAttributes>> = new Map<string, typeof CoreObject<CoreObjectAttributes>>();

/**
* Used by the Generator for adding an object instance with a unique API key into the Registrar
*/
public static register(objectInstance: typeof CoreObject<CoreObjectAttributes>): GlobalObjectPool {
this._globalMap.set(objectInstance.type, objectInstance);

return this;
}

/**
* Used by Reflective Constructors to re-generate objects from the API at runtime
*/
public static get(key: string): (typeof CoreObject<CoreObjectAttributes>) | null {
const obj: typeof CoreObject<CoreObjectAttributes> | undefined = this._globalMap.get(key);

return obj ? obj : null;
}

/**
* Generates a new instance of the object provided a key, otherwise returns null
*/
public static newInstance<T extends CoreObject<CoreObjectAttributes>>(key: string): T | null {
const obj: typeof CoreObject<CoreObjectAttributes> | null = this.get(key);

return obj ? obj.newInstance<T>() : null;
}
}
Loading

0 comments on commit 2149852

Please sign in to comment.