UNPKG

6.36 kBPlain TextView Raw
1import Boom = require("boom");
2import * as Bookshelf from "bookshelf";
3import { FetchOptions, FetchAllOptions, Collection } from "bookshelf";
4import Knex = require("knex");
5import Path = require("path");
6import dotenv = require("dotenv");
7import pg = require("pg");
8import { Controller } from "./controller";
9import { IRepository, IExtendedModel } from "./types";
10import { eventBus } from "./event-bus";
11import { Observable } from "rxjs/Observable";
12import "rxjs/add/operator/finally";
13
14dotenv.config();
15pg.types.setTypeParser(20, "text", parseInt);
16
17const env = process.env.NODE_ENV || "development";
18const rootPath = Path.resolve(process.cwd());
19const knexfilePath = Path.join(rootPath, "./knexfile");
20const knexfile = require(knexfilePath);
21const knex = Knex(knexfile[env]);
22
23export const bookshelf: any = Bookshelf(knex);
24
25bookshelf.plugin("registry");
26bookshelf.plugin("visibility");
27
28export const tableModelMap = {};
29
30function getDependencies(model) {
31 const dependencies = [];
32 if (!model || !model.relations) {
33 return;
34 }
35 const relations = [...Object.values(model.relations)];
36 while (relations.length) {
37 const relation: any = relations.pop();
38 let tableName;
39 if (
40 relation.head &&
41 typeof relation.head === "function" &&
42 relation.head().relatedData
43 ) {
44 tableName = relation.head().relatedData.targetTableName;
45 if (relation.head().relations) {
46 console.log(relation.head().relations);
47 Object.values(relation.head().relations).forEach(i =>
48 relations.push(i)
49 );
50 }
51 } else if (relation.relatedData && relation.relatedData.targetTableName) {
52 tableName = relation.relatedData.targetTableName;
53 if (relation.relations) {
54 console.log(relation.relations);
55 Object.values(relation.relations).forEach(i => relations.push(i));
56 }
57 }
58 console.log(relations);
59 console.log(tableName);
60 console.log(tableModelMap[tableName]);
61 if (!dependencies.includes(tableModelMap[tableName])) {
62 dependencies.push(tableModelMap[tableName]);
63 }
64 }
65 return dependencies;
66}
67
68function getCollectionDependencies(data: Collection<any>): string[] {
69 return getDependencies(data.head());
70}
71
72export class Repository implements IRepository {
73 public model: IExtendedModel;
74 public name: string;
75 public path: string;
76 public controller: Controller;
77 public websocket: boolean;
78 constructor(
79 path: string,
80 options: any,
81 name: string,
82 props: any,
83 websocket = false,
84 rest = true
85 ) {
86 tableModelMap[props.tableName] = name;
87 this.name = name;
88 this.path = path;
89 this.websocket = websocket;
90 props = {
91 ...props,
92 initialize() {
93 if (websocket) {
94 this.on("created", model => {
95 eventBus.emit(`DB_CHANGE:${name.toUpperCase()}`);
96 });
97 this.on("updated", model => {
98 eventBus.emit(`DB_CHANGE:${name.toUpperCase()}`);
99 });
100 }
101 }
102 };
103 this.model = bookshelf.model(name, props);
104 this.makeCrud(name, options);
105 this.makeReactive(name);
106
107 if (rest) {
108 this.controller = new Controller(path, (server, opts, next) => {
109 server.get("/", this.model.findAll);
110 server.get("/:id", this.model.findById);
111 server.post("/", this.model.upsert);
112 server.delete("/:id", this.model.remove);
113 server.post("/:id/undelete", this.model.recover);
114 });
115 }
116 }
117 public makeCrud(name: string, options: any) {
118 this.model.findAll = async () => {
119 return this.model.where("deleted", false).fetchAll(options) || [];
120 };
121 this.model.findById = async (request, reply) => {
122 const item = await this.model
123 .where("id", request.params.id)
124 .fetch(options);
125
126 if (!item) {
127 return Boom.badData(
128 `Entity ${name} with ID ${request.params.id} not exists`
129 );
130 }
131
132 return item;
133 };
134 this.model.upsert = async (request, reply) => {
135 const item = request.body;
136
137 if (!item) {
138 return Boom.badData(`Entity ${name}, Undefined Body`);
139 }
140
141 return this.model.forge(item).save();
142 };
143 this.model.remove = async (request, reply) => {
144 const id = request.params.id;
145
146 if (!id) {
147 return Boom.badData(`Entity ${name} undefined ID`);
148 }
149
150 const old = await this.model.where("id", id).fetch();
151
152 if (!old) {
153 return Boom.badData(`Entity ${name} with ID ${id} not exists`);
154 }
155
156 return old.save({ deleted: true });
157 };
158 this.model.recover = async (request, reply) => {
159 const item = request.body;
160
161 if (!item) {
162 return Boom.badData(`Entity ${name}, Undefined Body`);
163 }
164
165 const old = await this.model.where("id", item.id).fetch();
166
167 if (!old) {
168 return Boom.badData(`Entity ${name} with ID ${item.id} not exists`);
169 }
170
171 return old.save({ deleted: false });
172 };
173 }
174 public makeReactive(name: string) {
175 this.model.watch = (options?: FetchOptions): Observable<any> => {
176 const subscriptions = [];
177 return Observable.create(observer => {
178 this.model.fetch(options).then(data => {
179 observer.next(data);
180 const listenTo = [name, ...getDependencies(data)];
181 listenTo.map(l => l.toUpperCase()).forEach(dep => {
182 const sub = () =>
183 this.model.fetch(options).then(stream => observer.next(stream));
184 subscriptions.push(sub);
185 eventBus.on(`DB_CHANGE:${dep}`, sub);
186 });
187 });
188 }).finally(() => subscriptions.forEach(s => eventBus.unsubscribe(s)));
189 };
190 this.model.watchAll = (options?: FetchAllOptions): Observable<any> => {
191 const subscriptions = [];
192 return Observable.create(observer => {
193 this.model.fetchAll(options).then(data => {
194 observer.next(data);
195 const listenTo = [name, ...getCollectionDependencies(data)];
196 listenTo.map(l => l.toUpperCase()).forEach(dep => {
197 const sub = () =>
198 this.model
199 .fetchAll(options)
200 .then(stream => observer.next(stream));
201 subscriptions.push(sub);
202 eventBus.on(`DB_CHANGE:${dep}`, sub);
203 });
204 });
205 }).finally(() => subscriptions.forEach(s => eventBus.unsubscribe(s)));
206 };
207 }
208}