commit 831aeab9199e72bc4ef79f6de51329d30bcdfe1e Author: Abdussamed Date: Thu Oct 24 10:46:40 2024 +0300 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ed4cb8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +package-lock.json +data/* \ No newline at end of file diff --git a/core/database.js b/core/database.js new file mode 100644 index 0000000..3c4c646 --- /dev/null +++ b/core/database.js @@ -0,0 +1,67 @@ +import knex from "knex"; +import {resolve} from "node:path"; + +let EventDB = knex({ + client: "sqlite3", + connection:{ + filename: resolve(import.meta.dirname,"../data/events.db") + }, + useNullAsDefault: true +}); + +let CollectionDB = knex({ + client: "sqlite3", + connection:{ + filename: resolve(import.meta.dirname,"../data/collection.db") + }, + useNullAsDefault: true +}); + +export async function migrate() +{ + if(await EventDB.schema.hasTable("events") == false) + { + await EventDB.schema.createTable("events",function(table){ + + table.bigIncrements("id",{primaryKey: true}).unsigned(); + table.json("payload").notNullable(); + table.string("domain",255).index().notNullable(); + table.string("type",100).index().notNullable(); + table.datetime("time").notNullable(); + table.datetime("expire").notNullable(); + + }) + }; + + if(await CollectionDB.schema.hasTable("collection") == false) + { + await CollectionDB.schema.createTable("collection",function(table){ + + table.bigIncrements("id",{primaryKey: true}).unsigned(); + table.json("data").notNullable(); + table.string("catalog",255).index().notNullable(); + table.string("domain",255).index().notNullable(); + table.datetime("time").notNullable(); + table.datetime("expire").notNullable(); + + }) + } +} + +/** + * @returns {import("knex").Knex} + */ +export function Event() +{ + return EventDB.table("events"); +} + +/** + * @returns {import("knex").Knex} + */ +export function Collection() +{ + return CollectionDB.table("collection"); +} + +process.nextTick(migrate); \ No newline at end of file diff --git a/core/http.js b/core/http.js new file mode 100644 index 0000000..2087f01 --- /dev/null +++ b/core/http.js @@ -0,0 +1,33 @@ +import express from "express"; + +let app = express(); + +app.listen(6886,'0.0.0.0'); + +export const Application = app; + +/** + * @param {import("express").Request} request + * @param {import("express").Response} response + */ +export function cors() +{ + return (request, response, next) => { + response.setHeader("Access-Control-Allow-Origin","*"); + response.setHeader("Access-Control-Allow-Methods","GET, POST"); + response.setHeader("Access-Control-Allow-Credentials","false"); + response.setHeader("x-powered-by","Qulak/1.1"); + next(); + } +} + +/** + * @param {import("express").Request} request + * @param {import("express").Response} response + */ +export function jsonrequest() +{ + return express.json({ + limit: "5mb" + }) +} \ No newline at end of file diff --git a/core/publicfolder.js b/core/publicfolder.js new file mode 100644 index 0000000..402c021 --- /dev/null +++ b/core/publicfolder.js @@ -0,0 +1,13 @@ +import {Application} from "./http.js"; +import express from "express"; +import {resolve} from "node:path"; + +Application.use( + express.static( + resolve(import.meta.dirname,"../public"), + { + acceptRanges: true, + index: "main.html" + } + ) +) \ No newline at end of file diff --git a/init.js b/init.js new file mode 100644 index 0000000..3a3c193 --- /dev/null +++ b/init.js @@ -0,0 +1,5 @@ +import "./core/database.js"; +import "./core/http.js"; +import "./core/publicfolder.js"; +import "./src/collection.add.js"; +import "./src/collection.get.js"; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..84cd70f --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "collectioner", + "version": "1.0.0", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "express": "^4.21.1", + "joi": "^17.13.3", + "knex": "^3.1.0", + "moment": "^2.30.1", + "sqlite3": "^5.1.7" + } +} diff --git a/public/colletioner.js b/public/colletioner.js new file mode 100644 index 0000000..6c477d6 --- /dev/null +++ b/public/colletioner.js @@ -0,0 +1,78 @@ +async function AddCollection(catalog, data, expire = "1 year") +{ + if(typeof data != "object") throw new Error("data must be object"); + if(typeof catalog != "string") throw new Error("catalog must be string"); + + let pack = { + data: data, + domain: window.location.host, + expire: expire + }; + + let serializedData = JSON.stringify(pack); + + let url = new URL(`/collection/${catalog}/add`,window.location); + + let request = await fetch(url,{ + method: "post", + headers: { + 'accept': "text/json, application/json", + 'content-type': 'application/json' + }, + body: serializedData, + cache: "no-cache", + priority: "low", + referrerPolicy: "strict-origin", + redirect: "error" + }); + + if(request.ok == false) + { + throw await request.json() + } + + let response = await request.json(); + + if(response.status != "success") + { + throw new Error("Bir sorun oluştu ve veri eklenemedi") + } +} + +async function ReadCollection(catalog, order = "asc", length = 100, page = 1) +{ + if(typeof catalog != "string") throw new Error("catalog must be object"); + if(typeof order != "string") throw new Error("order must be string"); + if(typeof length != "number") throw new Error("length must be object"); + if(typeof page != "number") throw new Error("page must be object"); + + let serializedData = JSON.stringify({ + "order": order, + "length": length, + "page": page + }); + + let url = new URL(`/collection/${catalog}/data.json`,window.location); + + let request = await fetch(url,{ + method: "post", + headers: { + 'accept': "text/json, application/json", + 'content-type': 'application/json' + }, + body: serializedData, + cache: "no-cache", + priority: "low", + referrerPolicy: "strict-origin", + redirect: "error" + }); + + if(request.ok == false || request.status != 200) + { + throw await request.json() + } + + let response = await request.json(); + + return response; +} \ No newline at end of file diff --git a/public/main.html b/public/main.html new file mode 100644 index 0000000..845457c --- /dev/null +++ b/public/main.html @@ -0,0 +1,11 @@ + + + + + + Document + + + + + \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..ea6ea31 --- /dev/null +++ b/readme.md @@ -0,0 +1,31 @@ +## Collection + +Verileri depolayıp TAMAMINI herkese dağıtıp bir süre sonra geçersiz olacak veriler için kullanılır. Genel kullanım amacı cihazları birbirleri arasında eşitlemek için kullanılabiliir. + +- pagination özelliği vardır +- eventsource tüm verileri hızlıca aktarır +- Geçerlilik tarihine göre collection'un tamamı silinir +- Tek yönlüdür okuyan cihazlar feedback dönemezler + +## Events + +Tüm verileri kanallar üzerinden hızlıca herkese aktarır. Bu özellike bir bildirimi belli kanallar üzerinden tüm cihazları haberdar etmek için kullanılabilir. + +- Tarihli sistemdir belli bir tarihin sonrasında gerçekleşen olayları bildirir +- Bağlantıyı sağlayanlar hangi olayları dinleyeceklerini belirtirler ve yanlızca o olayları haberlerini alırlar +- Eventlerden sunucuya aksine bir rapor toplanabilir +- Temelde servis tarafında throttle uygulanır ve tüm olaylar dinleyicilere belirli periotlar ile dağıtılır +- Eventler tüm cihaza ulaştığı an veya geçerlilik süresi bittiğinde eventler silinir. +- Çift yönlüdür okuyan cihazlar veri hakkında feedback dönebilirler, feedbackler yavaşça sunucuya iletilir + + +## Notification + +Temel amacı çok fazla veriyi yavaşça en güncelden başlayarak en eskiye doğru tüm cihazlara dağıtmaktır. Temel olarak eventler ile arasındaki bir gruba değil nokta atışı bir veya aynı kullanıcı tarafından kullanılan birden fazla cihaza iletmektir. +Sunucu tarafından etiketlenen tüm kullanıcılara veriyi iletir +Mesaj tüm kullanıcılara iletildiğinde veriyi siler, iletilmediğinde geçerlilik tarihi bitene kadar depolar + +- Bağlanan cihazlar kendileri hakkında ayrıntılı bilgi verirler +- Notificationu gönderen sunucu hangi kesime veya kişiye göndereceğini bildirir ancak depolanırken tek tek depolanır +- Veri iletildiği an silinir veya iletilmediğinde bir süre sonra silinir +- Çift yönlüdür okuyan cihazlar veri hakkında feedback dönebilirler ve ayrıca sistem bildirimin iletildiğinide ayrıca tutabilir. Cihazların gönderdiği veriler sunucuya otomatik iletilmez, bunun yerine sunucu gelip alması gerekir \ No newline at end of file diff --git a/src/collection.add.js b/src/collection.add.js new file mode 100644 index 0000000..0ba8e2d --- /dev/null +++ b/src/collection.add.js @@ -0,0 +1,58 @@ +import Joi from "joi"; +import {Collection} from "../core/database.js"; +import {Application,cors, jsonrequest} from "../core/http.js"; +import moment from "moment"; + +Application.post("/collection/:catalog/add", jsonrequest(), cors(), AddCollection); + +/** + * @param {import("express").Request} request + * @param {import("express").Response} response + */ +async function AddCollection(request, response) +{ + let catalog = request.params.catalog; + let expression = Joi.object({ + data: Joi.object().required().max(873741824), + domain: Joi.string().max(200).min(5).required(), + expire: Joi.string().pattern(/^(\d{1,3})\s+(minute|hour|day|month|year)+$/).required() + }); + + let validate = expression.validate(request.body); + + if(validate.error) + { + return response.json({ + status: "fail", + message: "format not valid" + }).status(400); + } + + const { + data, + domain, + expire + } = request.body; + + let realExpire = parseRelativeTime(expire); + + await Collection().insert({ + data: data, + domain: domain, + time: moment().toISOString(), + expire: realExpire, + catalog: catalog + }); + + return response.status(200).json({ + status: "success", + }); +} + +function parseRelativeTime(relativeTime) { + const executedReg = /^(\d+)\s+(minute|hour|day|month|year)+$/g.exec(relativeTime); + + let time = moment().add(executedReg[1], executedReg[2]); + + return time.toISOString(); +} \ No newline at end of file diff --git a/src/collection.get.js b/src/collection.get.js new file mode 100644 index 0000000..b91ff56 --- /dev/null +++ b/src/collection.get.js @@ -0,0 +1,41 @@ +import moment from "moment"; +import {Collection} from "../core/database.js"; +import {Application,cors} from "../core/http.js"; + +Application.post("/collection/:catalog/data.json", cors(), ReadCollection); + +/** + * @param {import("express").Request} request + * @param {import("express").Response} response + */ +async function ReadCollection(request, response) +{ + let catalog = request.params.catalog; + let order = (request.body?.order == "asc" ? "asc" : "desc") || "asc"; + let length = typeof request.body?.length == "number" ? Math.max(1, Math.min(request.query.length, 250)) : 100; + let page = typeof request.body?.page == "number" ? Math.max(1, request.query.length) : 1; + let fromtime = typeof request.body?.fromtime == "string" ? parseRelativeTime(request.body.fromtime) : null; + + let build = Collection() + .where("catalog", catalog) + .where("expire", ">", moment().toISOString()) + .orderBy("time", order) + .offset((page - 1) * length) + .limit(length); + + if(fromtime) build.where("time",">", fromtime); + + let result = await build; + + let responseData = result.map(e => JSON.parse(e.data)); + + return response.status(200).json(responseData); +} + +function parseRelativeTime(relativeTime) { + const executedReg = /^(\d+)\s+(minute|hour|day|month|year)+$/g.exec(relativeTime); + + let time = moment().add(executedReg[1], executedReg[2]); + + return time.toISOString(); +} \ No newline at end of file