Compare commits

..

19 Commits

Author SHA1 Message Date
92dc3006a5
Merge pull request #9 from MarciiTheDev/dev
Update package.json - Version 1.2.1
2024-03-26 14:02:39 +01:00
8f87fb7bdf
Merge pull request #8 from MarciiTheDev/feature/Version1.2.1
Update package.json - Version 1.2.1
2024-03-26 13:57:07 +01:00
8002bc0226
Update package.json 2024-03-26 13:55:32 +01:00
898d3e9ebe
Merge pull request #7 from MarciiTheDev/dev
Added automatic reconnect for Websockets
2024-03-26 13:50:01 +01:00
e0c423a6c7
Merge pull request #6 from MarciiTheDev/bugfix/AutomaticWebsocketReconnect
Added automatic reconnect after connection with SignalR-Hub lost
2024-03-16 19:13:26 +01:00
830246b2e0 added automatic reconnect after connection with SignalR-Hub lost 2024-03-16 08:41:06 +01:00
e696c78790
Merge pull request #5 from MarciiTheDev/dev
Version 2.0.0 Release Merge
2023-12-06 16:51:55 +01:00
4ff41ebfec
Merge pull request #4 from MarciiTheDev/feature/GenericCollections
Added Generic Types - Version 2.0.0
2023-12-06 16:50:27 +01:00
770cc74b61 added Generic Types - Version 2.0.0 2023-12-06 16:44:54 +01:00
799668b83e Fixed Issue #3 & updated Version 2023-08-22 21:08:47 +02:00
4d28dcb23b Fixed Issue #2 & changed Version 2023-07-19 16:13:12 +02:00
f42f974e61 Updated MarcSync 2023-07-18 17:49:25 +02:00
4f4a6c7f23 Fixed Issue #1 & updated Version 2023-07-18 17:37:18 +02:00
a0e7e39207 Changed Version 2023-07-12 18:13:10 +02:00
e213d0cc08 Query Everything if no filter provided 2023-07-12 18:11:30 +02:00
1681f7b390 Added Documentation 2023-07-03 21:41:47 +02:00
17381406e0 Added Subscriptions 2023-07-03 21:36:24 +02:00
5c02b52e93 Created BaseEntry 2023-07-03 21:36:02 +02:00
de5bb35cb2 Changed Version 2023-07-03 21:35:34 +02:00
7 changed files with 180 additions and 68 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
package-lock.json package-lock.json
pnpm-lock.yaml
node_modules node_modules
dist dist
test test

View File

@ -2,8 +2,8 @@ const fs = require("fs");
const files = [] const files = []
fs.readdirSync("dist").filter( f => f.endsWith(".d.ts") ).forEach( f => { fs.readdirSync("dist").filter( f => f.endsWith(".js") ).forEach( f => {
files.push(`export * from "./${f}"`) files.push(`export * from "./${f.split(".")[0]}"`)
}); });
fs.writeFileSync("dist/index.d.ts", files.join("\n")) fs.writeFileSync("dist/index.d.ts", files.join("\n"))

View File

@ -1,6 +1,6 @@
{ {
"name": "marcsync", "name": "marcsync",
"version": "1.0.2-dev", "version": "1.2.1",
"description": "A NodeJS MarcSync Client to communicate with MarcSync's API", "description": "A NodeJS MarcSync Client to communicate with MarcSync's API",
"main": "dist/marcsync.js", "main": "dist/marcsync.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@ -23,6 +23,11 @@
}, },
"homepage": "https://github.com/MarciiTheDev/marcsync-nodejs-client#readme", "homepage": "https://github.com/MarciiTheDev/marcsync-nodejs-client#readme",
"devDependencies": { "devDependencies": {
"@types/node": "^20.3.3",
"dotenv": "^16.3.1",
"typescript": "^5.1.6" "typescript": "^5.1.6"
},
"dependencies": {
"@microsoft/signalr": "^7.0.7"
} }
} }

View File

@ -1,7 +1,7 @@
import { Entry, EntryData, EntryNotFound } from "./Entry"; import { Entry, EntryData, EntryNotFound } from "./Entry";
import { Unauthorized } from "./marcsync"; import { Unauthorized } from "./marcsync";
export class Collection { export class Collection<T extends EntryData> {
private _accessToken: string; private _accessToken: string;
private _collectionName: string; private _collectionName: string;
@ -149,7 +149,7 @@ export class Collection {
* }); * });
* *
*/ */
async createEntry(data: EntryData): Promise<Entry> { async createEntry(data: T): Promise<Entry<T>> {
try { try {
const result = await fetch(`https://api.marcsync.dev/v0/entries/${this._collectionName}`, { const result = await fetch(`https://api.marcsync.dev/v0/entries/${this._collectionName}`, {
method: "POST", method: "POST",
@ -185,7 +185,7 @@ export class Collection {
* *
* const entry = await collection.getEntryById("my-entry-id"); * const entry = await collection.getEntryById("my-entry-id");
*/ */
async getEntryById(id: string): Promise<Entry> { async getEntryById(id: string): Promise<Entry<T>> {
try { try {
const result = await fetch(`https://api.marcsync.dev/v1/entries/${this._collectionName}?methodOverwrite=GET`, { const result = await fetch(`https://api.marcsync.dev/v1/entries/${this._collectionName}?methodOverwrite=GET`, {
method: "PATCH", method: "PATCH",
@ -203,7 +203,7 @@ export class Collection {
const json = await result.json(); const json = await result.json();
if (!json.success) throw new Error(); if (!json.success) throw new Error();
if(json.entries.length === 0) throw new EntryNotFound(); if(json.entries.length === 0) throw new EntryNotFound();
return new Entry(this._accessToken, this._collectionName, json.entries[0]); return new Entry<T>(this._accessToken, this._collectionName, json.entries[0]);
} catch (e) { } catch (e) {
if(e instanceof Unauthorized) throw new Unauthorized(); if(e instanceof Unauthorized) throw new Unauthorized();
if(e instanceof EntryNotFound) throw new EntryNotFound(); if(e instanceof EntryNotFound) throw new EntryNotFound();
@ -236,7 +236,7 @@ export class Collection {
* @see {@link EntryData} for more information about entry data. * @see {@link EntryData} for more information about entry data.
* *
*/ */
async getEntries(filter: EntryData): Promise<Entry[]> { async getEntries(filter?: Partial<{ [K in keyof T]: T[K] }>): Promise<Entry<T>[]> {
try { try {
const result = await fetch(`https://api.marcsync.dev/v1/entries/${this._collectionName}?methodOverwrite=GET`, { const result = await fetch(`https://api.marcsync.dev/v1/entries/${this._collectionName}?methodOverwrite=GET`, {
method: "PATCH", method: "PATCH",
@ -245,7 +245,7 @@ export class Collection {
"content-type": "application/json" "content-type": "application/json"
}, },
body: JSON.stringify({ body: JSON.stringify({
filters: filter filters: filter || {}
}) })
}) })
if (result.status === 401) throw new Unauthorized(); if (result.status === 401) throw new Unauthorized();
@ -297,7 +297,7 @@ export class Collection {
* **__warning: Will delete the entries from the collection. This action cannot be undone.__** * **__warning: Will delete the entries from the collection. This action cannot be undone.__**
* *
*/ */
async deleteEntries(filter: EntryData): Promise<number> { async deleteEntries(filter?: Partial<{ [K in keyof T]: T[K] }>): Promise<number> {
try { try {
const result = await fetch(`https://api.marcsync.dev/v1/entries/${this._collectionName}`, { const result = await fetch(`https://api.marcsync.dev/v1/entries/${this._collectionName}`, {
method: "DELETE", method: "DELETE",
@ -324,7 +324,7 @@ export class Collection {
* @returns The Id of the updated entry * @returns The Id of the updated entry
* *
*/ */
async updateEntryById(id: string, data: EntryData): Promise<string> { async updateEntryById(id: string, data: Partial<{ [K in keyof T]: T[K] }>): Promise<string> {
try { try {
const result = await fetch(`https://api.marcsync.dev/v1/entries/${this._collectionName}`, { const result = await fetch(`https://api.marcsync.dev/v1/entries/${this._collectionName}`, {
method: "PUT", method: "PUT",
@ -355,7 +355,7 @@ export class Collection {
* @returns The amount of updated entries * @returns The amount of updated entries
* *
*/ */
async updateEntries(filter: EntryData, data: EntryData): Promise<number> { async updateEntries(filter: Partial<{ [K in keyof T]: T[K] }>, data: Partial<{ [K in keyof T]: T[K] }>): Promise<number> {
try { try {
const result = await fetch(`https://api.marcsync.dev/v1/entries/${this._collectionName}`, { const result = await fetch(`https://api.marcsync.dev/v1/entries/${this._collectionName}`, {
method: "PUT", method: "PUT",

View File

@ -1,15 +1,15 @@
export class Entry { type WithDefaultId<T extends EntryData> = T['_id'] extends string
? T
: T & { _id: string };
private _accessToken: string; export class BaseEntry<T extends EntryData> {
private _data: T;
private _collectionName: string; private _collectionName: string;
private _entryId: string;
private _data: EntryData;
constructor(accessToken: string, collectionName: string, data: EntryData) { constructor(private data: T, private collectionName: string) {
this._accessToken = accessToken;
this._collectionName = collectionName;
this._entryId = data._id;
this._data = data; this._data = data;
this._collectionName = collectionName;
} }
/** /**
@ -35,38 +35,10 @@ export class Entry {
* @see {@link EntryData} for more information about entry data. * @see {@link EntryData} for more information about entry data.
* *
*/ */
getValues(): EntryData { getValues(): WithDefaultId<T> {
return this._data; return this._data as WithDefaultId<T>;
} }
/**
*
* @param key - The key of the value to get
* @returns The value of the specified key
*
* @example
*
* import { Client } from "marcsync";
*
* const client = new Client("<my access token>");
* const collection = client.getCollection("my-collection");
*
* const entry = await collection.getEntryById("my-entry-id");
*
* const name = entry.getValueAs<string>("name");
*
* console.log(name);
*
* @remarks
* This method is useful if you want to get the value of a specific key as a specific type.
*
* @see {@link EntryData} for more information about entry data.
*
*/
getValueAs<T>(key: string): T {
return this._data[key];
}
/** /**
* *
* @param key - The key of the value to get * @param key - The key of the value to get
@ -91,8 +63,33 @@ export class Entry {
* @see {@link EntryData} for more information about entry data. * @see {@link EntryData} for more information about entry data.
* *
*/ */
getValue(key: string): any { getValue<K extends keyof WithDefaultId<T>>(key: K): WithDefaultId<T>[K] {
return this._data[key]; return (this._data as WithDefaultId<T>)[key];
}
/**
*
* @returns The name of the collection of the entry
*
*/
getCollectionName(): string {
return this._collectionName;
}
protected _setData(data: T) {
this._data = data;
}
}
export class Entry<T extends EntryData> extends BaseEntry<T> {
private _accessToken: string;
private _entryId: string;
constructor(accessToken: string, collectionName: string, data: T) {
super(data, collectionName);
this._accessToken = accessToken;
this._entryId = data._id!;
} }
/** /**
@ -118,9 +115,9 @@ export class Entry {
* This method is useful if you want to update the value of a specific key. * This method is useful if you want to update the value of a specific key.
* *
*/ */
async updateValue(key: string, value: any): Promise<EntryData> { async updateValue<K extends keyof WithDefaultId<T>>(key: K, value: WithDefaultId<T>[K]): Promise<WithDefaultId<T>> {
try { try {
const result = await fetch(`https://api.marcsync.dev/v1/entries/${this._collectionName}`, { const result = await fetch(`https://api.marcsync.dev/v1/entries/${this.getCollectionName()}`, {
method: "PUT", method: "PUT",
headers: { headers: {
authorization: this._accessToken, authorization: this._accessToken,
@ -140,8 +137,10 @@ export class Entry {
} catch (err) { } catch (err) {
throw new EntryUpdateFailed(err); throw new EntryUpdateFailed(err);
} }
this._data[key] = value; const data = this.getValues();
return this._data; data[key] = value;
this._setData(data);
return data;
} }
/** /**
@ -170,9 +169,9 @@ export class Entry {
* @see {@link updateValue} for more information about updating a single value. * @see {@link updateValue} for more information about updating a single value.
* *
*/ */
async updateValues(values: EntryData): Promise<EntryData> { async updateValues(values: Partial<{ [K in keyof WithDefaultId<T>]: WithDefaultId<T>[K] }>): Promise<WithDefaultId<T>> {
try { try {
const result = await fetch(`https://api.marcsync.dev/v1/entries/${this._collectionName}`, { const result = await fetch(`https://api.marcsync.dev/v1/entries/${this.getCollectionName()}`, {
method: "PUT", method: "PUT",
headers: { headers: {
authorization: this._accessToken, authorization: this._accessToken,
@ -190,10 +189,12 @@ export class Entry {
} catch (err) { } catch (err) {
throw new EntryUpdateFailed(err); throw new EntryUpdateFailed(err);
} }
const data = this.getValues();
for (const key in values) { for (const key in values) {
this._data[key] = values[key]; data[key] = values[key]!;
} }
return this._data; this._setData(data);
return data;
} }
/** /**
@ -203,7 +204,7 @@ export class Entry {
*/ */
async delete(): Promise<void> { async delete(): Promise<void> {
try { try {
const result = await fetch(`https://api.marcsync.dev/v1/entries/${this._collectionName}`, { const result = await fetch(`https://api.marcsync.dev/v1/entries/${this.getCollectionName()}`, {
method: "DELETE", method: "DELETE",
headers: { headers: {
authorization: this._accessToken, authorization: this._accessToken,
@ -224,6 +225,7 @@ export class Entry {
} }
export interface EntryData { export interface EntryData {
_id?: string
[key: string]: any; [key: string]: any;
} }

View File

@ -0,0 +1,83 @@
import { BaseEntry, Entry, EntryData } from "./Entry";
import { ClientEvents } from "./marcsync";
import * as signalR from "@microsoft/signalr";
export class SubscriptionManager {
private _subscriptions: Record<keyof ClientEvents, ((...args: any) => void)[]>;
private _hubConnection: signalR.HubConnection;
private _accessToken: string;
constructor(accessToken: string) {
this._accessToken = accessToken;
this._subscriptions = {
entryCreated: [],
entryDeleted: [],
entryUpdated: []
} as Record<keyof ClientEvents, ((...args: any) => void)[]>;
this._hubConnection = new signalR.HubConnectionBuilder()
.withUrl("https://ws.marcsync.dev/websocket?access_token=Bearer " + accessToken, {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets
})
.withAutomaticReconnect([0, 2000, 10000, 30000, 60000])
.configureLogging(signalR.LogLevel.None)
.build();
this._hubConnection.start()
.catch(err => {
console.error(err.toString());
process.exit(1);
});
this.handleSubscriptions();
}
subscribe(subscription: keyof ClientEvents, callback: () => void) {
if (!this._subscriptions[subscription]) this._subscriptions[subscription] = [];
this._subscriptions[subscription].push(callback);
}
private async handleSubscriptions() {
this._hubConnection.on("entryCreated", (e: string) => {
let d = JSON.parse(e) as EntryCreatedEvent;
this._subscriptions.entryCreated.forEach(callback => { try { callback(new Entry(this._accessToken, d.data.collectionName, d.data.values), d.databaseId, d.timestamp) } catch(e) {console.error(e)} });
});
this._hubConnection.on("entryDeleted", (e: string) => {
let d = JSON.parse(e) as EntryDeletedEvent;
this._subscriptions.entryDeleted.forEach(callback => { try { callback(new BaseEntry(d.data.values, d.data.collectionName), d.databaseId, d.timestamp) } catch(e) {console.error(e)} });
})
this._hubConnection.on("entryUpdated", (e: string) => {
let d = JSON.parse(e) as EntryUpdatedEvent;
this._subscriptions.entryUpdated.forEach(callback => { try { callback(new BaseEntry(d.data.oldValues, d.data.collectionName), new Entry(this._accessToken, d.data.collectionName, d.data.newValues), d.databaseId, d.timestamp) } catch(e) {console.error(e)} });
})
}
}
export interface BaseEvent {
databaseId: string;
timestamp: number;
type: number;
}
export interface EntryCreatedEvent extends BaseEvent {
data: {
collectionName: string;
values: EntryData;
};
}
export interface EntryDeletedEvent extends BaseEvent {
data: {
collectionName: string;
values: EntryData;
};
}
export interface EntryUpdatedEvent extends BaseEvent {
data: {
collectionName: string;
oldValues: EntryData;
newValues: EntryData;
};
}

View File

@ -1,9 +1,11 @@
import { Collection, CollectionAlreadyExists, CollectionNotFound } from "./Collection"; import { Collection, CollectionAlreadyExists, CollectionNotFound } from "./Collection";
import { Entry, EntryData } from "./Entry"; import { BaseEntry, Entry, EntryData } from "./Entry";
import { SubscriptionManager } from "./SubscriptionManager";
export class Client { export class Client {
private _accessToken: string; private _accessToken: string;
private _subscriptions: SubscriptionManager;
/** /**
* *
@ -12,6 +14,7 @@ export class Client {
* *
*/ */
constructor(accessToken: string) { constructor(accessToken: string) {
this._subscriptions = new SubscriptionManager(accessToken);
this._accessToken = accessToken; this._accessToken = accessToken;
} }
@ -28,8 +31,8 @@ export class Client {
* const collection = client.getCollection("my-collection"); * const collection = client.getCollection("my-collection");
* *
*/ */
getCollection(collectionName: string) { getCollection<T extends EntryData = EntryData>(collectionName: string): Collection<T> {
return new Collection(this._accessToken, collectionName); return new Collection<T>(this._accessToken, collectionName);
} }
/** /**
@ -48,7 +51,7 @@ export class Client {
* This method is useful if you want to fetch the collection from the server to check if it exists before using it. * This method is useful if you want to fetch the collection from the server to check if it exists before using it.
* *
*/ */
async fetchCollection(collectionName: string): Promise<Collection> { async fetchCollection<T extends EntryData = EntryData>(collectionName: string): Promise<Collection<T>> {
try { try {
const result = await fetch(`https://api.marcsync.dev/v0/collection/${collectionName}`, { const result = await fetch(`https://api.marcsync.dev/v0/collection/${collectionName}`, {
method: "GET", method: "GET",
@ -63,7 +66,7 @@ export class Client {
if (e instanceof Unauthorized) throw new Unauthorized(); if (e instanceof Unauthorized) throw new Unauthorized();
throw new CollectionNotFound(); throw new CollectionNotFound();
} }
return new Collection(this._accessToken, collectionName); return new Collection<T>(this._accessToken, collectionName);
} }
/** /**
@ -80,7 +83,7 @@ export class Client {
* *
* @remarks * @remarks
*/ */
async createCollection(collectionName: string): Promise<Collection> { async createCollection<T extends EntryData = EntryData>(collectionName: string): Promise<Collection<T>> {
try { try {
const result = await fetch(`https://api.marcsync.dev/v0/collection/${collectionName}`, { const result = await fetch(`https://api.marcsync.dev/v0/collection/${collectionName}`, {
method: "POST", method: "POST",
@ -97,10 +100,28 @@ export class Client {
} }
return new Collection(this._accessToken, collectionName); return new Collection(this._accessToken, collectionName);
} }
/**
*
* @param event - The event to listen to
* @param listener - The listener to call when the event is emitted
* @returns The client instance
*
*/
public on<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): this {
this._subscriptions.subscribe(event, listener);
return this;
};
} }
export class Unauthorized extends Error { export class Unauthorized extends Error {
constructor(message: string = "Invalid access token") { constructor(message: string = "Invalid access token") {
super(message); super(message);
} }
}
export interface ClientEvents {
entryCreated: [entry: Entry<EntryData>, databaseId: string, timestamp: number];
entryUpdated: [oldEntry: BaseEntry<EntryData>, newEntry: Entry<EntryData>, databaseId: string, timestamp: number];
entryDeleted: [entry: BaseEntry<EntryData>, databaseId: string, timestamp: number];
} }