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