UNPKG

6.82 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
30// function getModelDependencies(data: any): string[] {
31// const dependencies = [];
32
33// Object.values(data.relations).forEach((relation: any) => {
34// const tableName = relation.relatedData.targetTableName;
35// dependencies.push(tableModelMap[tableName]);
36// });
37
38// return dependencies;
39// }
40
41// function getCollectionDependencies(data: Collection<any>): string[] {
42// const dependencies = [];
43
44// Object.values(data.head().relations).forEach((relation: any) => {
45// const tableName = relation.relatedData.targetTableName;
46// dependencies.push(tableModelMap[tableName]);
47// });
48
49// return dependencies;
50// }
51
52function getDependencies(model) {
53 const dependencies = [];
54 if (!model || !model.relations) {
55 return;
56 }
57 const relations = [...Object.values(model.relations)];
58 while (relations.length) {
59 const relation: any = relations.pop();
60 let tableName;
61 if (
62 relation.head &&
63 typeof relation.head === "function" &&
64 relation.head().relatedData
65 ) {
66 tableName = relation.head().relatedData.targetTableName;
67 if (relation.head().relations) {
68 Object.values(relation.head().relations).forEach(i =>
69 relations.push(i)
70 );
71 }
72 } else if (relation.relatedData && relation.relatedData.targetTableName) {
73 tableName = relation.relatedData.targetTableName;
74 if (relation.relations) {
75 Object.values(relation.relations).forEach(i => relations.push(i));
76 }
77 }
78 if (!dependencies.includes(tableModelMap[tableName])) {
79 dependencies.push(tableModelMap[tableName]);
80 }
81 }
82 return dependencies;
83}
84
85function getCollectionDependencies(data: Collection<any>): string[] {
86 return getDependencies(data.head());
87}
88
89export class Repository implements IRepository {
90 public model: IExtendedModel;
91 public name: string;
92 public path: string;
93 public controller: Controller;
94 public websocket: boolean;
95 constructor(
96 path: string,
97 options: any,
98 name: string,
99 props: any,
100 websocket = false,
101 rest = true
102 ) {
103 tableModelMap[props.tableName] = name;
104 this.name = name;
105 this.path = path;
106 this.websocket = websocket;
107 props = {
108 ...props,
109 initialize() {
110 if (websocket) {
111 this.on("created", model => {
112 eventBus.emit(`DB_CHANGE:${name.toUpperCase()}`);
113 });
114 this.on("updated", model => {
115 eventBus.emit(`DB_CHANGE:${name.toUpperCase()}`);
116 });
117 }
118 }
119 };
120 this.model = bookshelf.model(name, props);
121 this.makeCrud(name, options);
122 this.makeReactive(name);
123
124 if (rest) {
125 this.controller = new Controller(path, (server, opts, next) => {
126 server.get("/", this.model.findAll);
127 server.get("/:id", this.model.findById);
128 server.post("/", this.model.upsert);
129 server.delete("/:id", this.model.remove);
130 server.post("/:id/undelete", this.model.recover);
131 });
132 }
133 }
134 public makeCrud(name: string, options: any) {
135 this.model.findAll = async () => {
136 return this.model.where("deleted", false).fetchAll(options) || [];
137 };
138 this.model.findById = async (request, reply) => {
139 const item = await this.model
140 .where("id", request.params.id)
141 .fetch(options);
142
143 if (!item) {
144 return Boom.badData(
145 `Entity ${name} with ID ${request.params.id} not exists`
146 );
147 }
148
149 return item;
150 };
151 this.model.upsert = async (request, reply) => {
152 const item = request.body;
153
154 if (!item) {
155 return Boom.badData(`Entity ${name}, Undefined Body`);
156 }
157
158 return this.model.forge(item).save();
159 };
160 this.model.remove = async (request, reply) => {
161 const id = request.params.id;
162
163 if (!id) {
164 return Boom.badData(`Entity ${name} undefined ID`);
165 }
166
167 const old = await this.model.where("id", id).fetch();
168
169 if (!old) {
170 return Boom.badData(`Entity ${name} with ID ${id} not exists`);
171 }
172
173 return old.save({ deleted: true });
174 };
175 this.model.recover = async (request, reply) => {
176 const item = request.body;
177
178 if (!item) {
179 return Boom.badData(`Entity ${name}, Undefined Body`);
180 }
181
182 const old = await this.model.where("id", item.id).fetch();
183
184 if (!old) {
185 return Boom.badData(`Entity ${name} with ID ${item.id} not exists`);
186 }
187
188 return old.save({ deleted: false });
189 };
190 }
191 public makeReactive(name: string) {
192 this.model.watch = (options?: FetchOptions): Observable<any> => {
193 const subscriptions = [];
194 return Observable.create(observer => {
195 this.model.fetch(options).then(data => {
196 observer.next(data);
197 const listenTo = [name, ...getDependencies(data)];
198 listenTo.map(l => l.toUpperCase()).forEach(dep => {
199 const sub = () =>
200 this.model.fetch(options).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 this.model.watchAll = (options?: FetchAllOptions): Observable<any> => {
208 const subscriptions = [];
209 return Observable.create(observer => {
210 this.model.fetchAll(options).then(data => {
211 observer.next(data);
212 const listenTo = [name, ...getCollectionDependencies(data)];
213 listenTo.map(l => l.toUpperCase()).forEach(dep => {
214 const sub = () =>
215 this.model
216 .fetchAll(options)
217 .then(stream => observer.next(stream));
218 subscriptions.push(sub);
219 eventBus.on(`DB_CHANGE:${dep}`, sub);
220 });
221 });
222 }).finally(() => subscriptions.forEach(s => eventBus.unsubscribe(s)));
223 };
224 }
225}