forked from tsai/budibase
98 changed files with 2014 additions and 576 deletions
@ -1,5 +1,6 @@ |
|||||
const env = require("../src/environment") |
const env = require("../src/environment") |
||||
|
|
||||
|
env._set("SELF_HOSTED", "1") |
||||
env._set("NODE_ENV", "jest") |
env._set("NODE_ENV", "jest") |
||||
env._set("JWT_SECRET", "test-jwtsecret") |
env._set("JWT_SECRET", "test-jwtsecret") |
||||
env._set("LOG_LEVEL", "silent") |
env._set("LOG_LEVEL", "silent") |
||||
|
|||||
@ -0,0 +1,22 @@ |
|||||
|
const API = require("./api") |
||||
|
const env = require("../environment") |
||||
|
|
||||
|
const api = new API(env.ACCOUNT_PORTAL_URL) |
||||
|
|
||||
|
// TODO: Authorization
|
||||
|
|
||||
|
exports.getAccount = async email => { |
||||
|
const payload = { |
||||
|
email, |
||||
|
} |
||||
|
const response = await api.post(`/api/accounts/search`, { |
||||
|
body: payload, |
||||
|
}) |
||||
|
const json = await response.json() |
||||
|
|
||||
|
if (response.status !== 200) { |
||||
|
throw Error(`Error getting account by email ${email}`, json) |
||||
|
} |
||||
|
|
||||
|
return json[0] |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
const fetch = require("node-fetch") |
||||
|
class API { |
||||
|
constructor(host) { |
||||
|
this.host = host |
||||
|
} |
||||
|
|
||||
|
apiCall = |
||||
|
method => |
||||
|
async (url = "", options = {}) => { |
||||
|
if (!options.headers) { |
||||
|
options.headers = {} |
||||
|
} |
||||
|
|
||||
|
if (!options.headers["Content-Type"]) { |
||||
|
options.headers = { |
||||
|
"Content-Type": "application/json", |
||||
|
Accept: "application/json", |
||||
|
...options.headers, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let json = options.headers["Content-Type"] === "application/json" |
||||
|
|
||||
|
const requestOptions = { |
||||
|
method: method, |
||||
|
body: json ? JSON.stringify(options.body) : options.body, |
||||
|
headers: options.headers, |
||||
|
// TODO: See if this is necessary
|
||||
|
credentials: "include", |
||||
|
} |
||||
|
|
||||
|
const resp = await fetch(`${this.host}${url}`, requestOptions) |
||||
|
|
||||
|
return resp |
||||
|
} |
||||
|
|
||||
|
post = this.apiCall("POST") |
||||
|
get = this.apiCall("GET") |
||||
|
patch = this.apiCall("PATCH") |
||||
|
del = this.apiCall("DELETE") |
||||
|
put = this.apiCall("PUT") |
||||
|
} |
||||
|
|
||||
|
module.exports = API |
||||
@ -1,139 +0,0 @@ |
|||||
import * as Sentry from "@sentry/browser" |
|
||||
import posthog from "posthog-js" |
|
||||
import api from "builderStore/api" |
|
||||
|
|
||||
let analyticsEnabled |
|
||||
const posthogConfigured = process.env.POSTHOG_TOKEN && process.env.POSTHOG_URL |
|
||||
const sentryConfigured = process.env.SENTRY_DSN |
|
||||
|
|
||||
const FEEDBACK_SUBMITTED_KEY = "budibase:feedback_submitted" |
|
||||
const APP_FIRST_STARTED_KEY = "budibase:first_run" |
|
||||
const feedbackHours = 12 |
|
||||
|
|
||||
async function activate() { |
|
||||
if (analyticsEnabled === undefined) { |
|
||||
// only the server knows the true NODE_ENV
|
|
||||
// this was an issue as NODE_ENV = 'cypress' on the server,
|
|
||||
// but 'production' on the client
|
|
||||
const response = await api.get("/api/analytics") |
|
||||
analyticsEnabled = (await response.json()).enabled === true |
|
||||
} |
|
||||
if (!analyticsEnabled) return |
|
||||
if (sentryConfigured) Sentry.init({ dsn: process.env.SENTRY_DSN }) |
|
||||
if (posthogConfigured) { |
|
||||
posthog.init(process.env.POSTHOG_TOKEN, { |
|
||||
autocapture: false, |
|
||||
capture_pageview: false, |
|
||||
api_host: process.env.POSTHOG_URL, |
|
||||
}) |
|
||||
posthog.set_config({ persistence: "cookie" }) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function identify(id) { |
|
||||
if (!analyticsEnabled || !id) return |
|
||||
if (posthogConfigured) posthog.identify(id) |
|
||||
if (sentryConfigured) |
|
||||
Sentry.configureScope(scope => { |
|
||||
scope.setUser({ id: id }) |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
async function identifyByApiKey(apiKey) { |
|
||||
if (!analyticsEnabled) return true |
|
||||
try { |
|
||||
const response = await fetch( |
|
||||
`https://03gaine137.execute-api.eu-west-1.amazonaws.com/prod/account/id?api_key=${apiKey.trim()}` |
|
||||
) |
|
||||
if (response.status === 200) { |
|
||||
const id = await response.json() |
|
||||
|
|
||||
await api.put("/api/keys/userId", { value: id }) |
|
||||
identify(id) |
|
||||
return true |
|
||||
} |
|
||||
|
|
||||
return false |
|
||||
} catch (error) { |
|
||||
console.log(error) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function captureException(err) { |
|
||||
if (!analyticsEnabled) return |
|
||||
Sentry.captureException(err) |
|
||||
captureEvent("Error", { error: err.message ? err.message : err }) |
|
||||
} |
|
||||
|
|
||||
function captureEvent(eventName, props = {}) { |
|
||||
if (!analyticsEnabled || !process.env.POSTHOG_TOKEN) return |
|
||||
props.sourceApp = "builder" |
|
||||
posthog.capture(eventName, props) |
|
||||
} |
|
||||
|
|
||||
if (!localStorage.getItem(APP_FIRST_STARTED_KEY)) { |
|
||||
localStorage.setItem(APP_FIRST_STARTED_KEY, Date.now()) |
|
||||
} |
|
||||
|
|
||||
const isFeedbackTimeElapsed = sinceDateStr => { |
|
||||
const sinceDate = parseFloat(sinceDateStr) |
|
||||
const feedbackMilliseconds = feedbackHours * 60 * 60 * 1000 |
|
||||
return Date.now() > sinceDate + feedbackMilliseconds |
|
||||
} |
|
||||
|
|
||||
function submitFeedback(values) { |
|
||||
if (!analyticsEnabled || !process.env.POSTHOG_TOKEN) return |
|
||||
localStorage.setItem(FEEDBACK_SUBMITTED_KEY, Date.now()) |
|
||||
|
|
||||
const prefixedValues = Object.entries(values).reduce((obj, [key, value]) => { |
|
||||
obj[`feedback_${key}`] = value |
|
||||
return obj |
|
||||
}, {}) |
|
||||
|
|
||||
posthog.capture("Feedback Submitted", prefixedValues) |
|
||||
} |
|
||||
|
|
||||
function requestFeedbackOnDeploy() { |
|
||||
if (!analyticsEnabled || !process.env.POSTHOG_TOKEN) return false |
|
||||
const lastSubmittedStr = localStorage.getItem(FEEDBACK_SUBMITTED_KEY) |
|
||||
if (!lastSubmittedStr) return true |
|
||||
return isFeedbackTimeElapsed(lastSubmittedStr) |
|
||||
} |
|
||||
|
|
||||
function highlightFeedbackIcon() { |
|
||||
if (!analyticsEnabled || !process.env.POSTHOG_TOKEN) return false |
|
||||
const lastSubmittedStr = localStorage.getItem(FEEDBACK_SUBMITTED_KEY) |
|
||||
if (lastSubmittedStr) return isFeedbackTimeElapsed(lastSubmittedStr) |
|
||||
const firstRunStr = localStorage.getItem(APP_FIRST_STARTED_KEY) |
|
||||
if (!firstRunStr) return false |
|
||||
return isFeedbackTimeElapsed(firstRunStr) |
|
||||
} |
|
||||
|
|
||||
// Opt In/Out
|
|
||||
const ifAnalyticsEnabled = func => () => { |
|
||||
if (analyticsEnabled && process.env.POSTHOG_TOKEN) { |
|
||||
return func() |
|
||||
} |
|
||||
} |
|
||||
const disabled = () => posthog.has_opted_out_capturing() |
|
||||
const optIn = () => posthog.opt_in_capturing() |
|
||||
const optOut = () => posthog.opt_out_capturing() |
|
||||
|
|
||||
export default { |
|
||||
activate, |
|
||||
identify, |
|
||||
identifyByApiKey, |
|
||||
captureException, |
|
||||
captureEvent, |
|
||||
requestFeedbackOnDeploy, |
|
||||
submitFeedback, |
|
||||
highlightFeedbackIcon, |
|
||||
disabled: () => { |
|
||||
if (analyticsEnabled == null) { |
|
||||
return true |
|
||||
} |
|
||||
return ifAnalyticsEnabled(disabled) |
|
||||
}, |
|
||||
optIn: ifAnalyticsEnabled(optIn), |
|
||||
optOut: ifAnalyticsEnabled(optOut), |
|
||||
} |
|
||||
@ -0,0 +1,94 @@ |
|||||
|
export default class IntercomClient { |
||||
|
constructor(token) { |
||||
|
this.token = token |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Instantiate intercom using their provided script. |
||||
|
*/ |
||||
|
init() { |
||||
|
if (!this.token) return |
||||
|
|
||||
|
const token = this.token |
||||
|
|
||||
|
var w = window |
||||
|
var ic = w.Intercom |
||||
|
if (typeof ic === "function") { |
||||
|
ic("reattach_activator") |
||||
|
ic("update", w.intercomSettings) |
||||
|
} else { |
||||
|
var d = document |
||||
|
var i = function () { |
||||
|
i.c(arguments) |
||||
|
} |
||||
|
i.q = [] |
||||
|
i.c = function (args) { |
||||
|
i.q.push(args) |
||||
|
} |
||||
|
w.Intercom = i |
||||
|
var l = function () { |
||||
|
var s = d.createElement("script") |
||||
|
s.type = "text/javascript" |
||||
|
s.async = true |
||||
|
s.src = "https://widget.intercom.io/widget/" + token |
||||
|
var x = d.getElementsByTagName("script")[0] |
||||
|
x.parentNode.insertBefore(s, x) |
||||
|
} |
||||
|
if (document.readyState === "complete") { |
||||
|
l() |
||||
|
} else if (w.attachEvent) { |
||||
|
w.attachEvent("onload", l) |
||||
|
} else { |
||||
|
w.addEventListener("load", l, false) |
||||
|
} |
||||
|
|
||||
|
this.initialised = true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Show the intercom chat bubble. |
||||
|
* @param {Object} user - user to identify |
||||
|
* @returns Intercom global object |
||||
|
*/ |
||||
|
show(user = {}) { |
||||
|
if (!this.initialised) return |
||||
|
|
||||
|
return window.Intercom("boot", { |
||||
|
app_id: this.token, |
||||
|
...user, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Update intercom user details and messages. |
||||
|
* @returns Intercom global object |
||||
|
*/ |
||||
|
update() { |
||||
|
if (!this.initialised) return |
||||
|
|
||||
|
return window.Intercom("update") |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Capture analytics events and send them to intercom. |
||||
|
* @param {String} event - event identifier |
||||
|
* @param {Object} props - properties for the event |
||||
|
* @returns Intercom global object |
||||
|
*/ |
||||
|
captureEvent(event, props = {}) { |
||||
|
if (!this.initialised) return |
||||
|
|
||||
|
return window.Intercom("trackEvent", event, props) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Disassociate the user from the current session. |
||||
|
* @returns Intercom global object |
||||
|
*/ |
||||
|
logout() { |
||||
|
if (!this.initialised) return |
||||
|
|
||||
|
return window.Intercom("shutdown") |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
import posthog from "posthog-js" |
||||
|
import { Events } from "./constants" |
||||
|
|
||||
|
export default class PosthogClient { |
||||
|
constructor(token, url) { |
||||
|
this.token = token |
||||
|
this.url = url |
||||
|
} |
||||
|
|
||||
|
init() { |
||||
|
if (!this.token || !this.url) return |
||||
|
|
||||
|
posthog.init(this.token, { |
||||
|
autocapture: false, |
||||
|
capture_pageview: false, |
||||
|
api_host: this.url, |
||||
|
}) |
||||
|
posthog.set_config({ persistence: "cookie" }) |
||||
|
|
||||
|
this.initialised = true |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the posthog context to the current user |
||||
|
* @param {String} id - unique user id |
||||
|
*/ |
||||
|
identify(id) { |
||||
|
if (!this.initialised) return |
||||
|
|
||||
|
posthog.identify(id) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Update user metadata associated with current user in posthog |
||||
|
* @param {Object} meta - user fields |
||||
|
*/ |
||||
|
updateUser(meta) { |
||||
|
if (!this.initialised) return |
||||
|
|
||||
|
posthog.people.set(meta) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Capture analytics events and send them to posthog. |
||||
|
* @param {String} event - event identifier |
||||
|
* @param {Object} props - properties for the event |
||||
|
*/ |
||||
|
captureEvent(eventName, props) { |
||||
|
if (!this.initialised) return |
||||
|
|
||||
|
props.sourceApp = "builder" |
||||
|
posthog.capture(eventName, props) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Submit NPS feedback to posthog. |
||||
|
* @param {Object} values - NPS Values |
||||
|
*/ |
||||
|
npsFeedback(values) { |
||||
|
if (!this.initialised) return |
||||
|
|
||||
|
localStorage.setItem(Events.NPS.SUBMITTED, Date.now()) |
||||
|
|
||||
|
const prefixedFeedback = {} |
||||
|
for (let key in values) { |
||||
|
prefixedFeedback[`feedback_${key}`] = values[key] |
||||
|
} |
||||
|
|
||||
|
posthog.capture(Events.NPS.SUBMITTED, prefixedFeedback) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Reset posthog user back to initial state on logout. |
||||
|
*/ |
||||
|
logout() { |
||||
|
if (!this.initialised) return |
||||
|
|
||||
|
posthog.reset() |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
import * as Sentry from "@sentry/browser" |
||||
|
|
||||
|
export default class SentryClient { |
||||
|
constructor(dsn) { |
||||
|
this.dsn = dsn |
||||
|
} |
||||
|
|
||||
|
init() { |
||||
|
if (this.dsn) { |
||||
|
Sentry.init({ dsn: this.dsn }) |
||||
|
|
||||
|
this.initalised = true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Capture an exception and send it to sentry. |
||||
|
* @param {Error} err - JS error object |
||||
|
*/ |
||||
|
captureException(err) { |
||||
|
if (!this.initalised) return |
||||
|
|
||||
|
Sentry.captureException(err) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Identify user in sentry. |
||||
|
* @param {String} id - Unique user id |
||||
|
*/ |
||||
|
identify(id) { |
||||
|
if (!this.initalised) return |
||||
|
|
||||
|
Sentry.configureScope(scope => { |
||||
|
scope.setUser({ id }) |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
export const Events = { |
||||
|
BUILDER: { |
||||
|
STARTED: "Builder Started", |
||||
|
}, |
||||
|
COMPONENT: { |
||||
|
CREATED: "Added Component", |
||||
|
}, |
||||
|
DATASOURCE: { |
||||
|
CREATED: "Datasource Created", |
||||
|
UPDATED: "Datasource Updated", |
||||
|
}, |
||||
|
TABLE: { |
||||
|
CREATED: "Table Created", |
||||
|
}, |
||||
|
VIEW: { |
||||
|
CREATED: "View Created", |
||||
|
ADDED_FILTER: "Added View Filter", |
||||
|
ADDED_CALCULATE: "Added View Calculate", |
||||
|
}, |
||||
|
SCREEN: { |
||||
|
CREATED: "Screen Created", |
||||
|
}, |
||||
|
AUTOMATION: { |
||||
|
CREATED: "Automation Created", |
||||
|
SAVED: "Automation Saved", |
||||
|
BLOCK_ADDED: "Added Automation Block", |
||||
|
}, |
||||
|
NPS: { |
||||
|
SUBMITTED: "budibase:feedback_submitted", |
||||
|
}, |
||||
|
APP: { |
||||
|
CREATED: "budibase:app_created", |
||||
|
PUBLISHED: "budibase:app_published", |
||||
|
UNPUBLISHED: "budibase:app_unpublished", |
||||
|
}, |
||||
|
ANALYTICS: { |
||||
|
OPT_IN: "budibase:analytics_opt_in", |
||||
|
OPT_OUT: "budibase:analytics_opt_out", |
||||
|
}, |
||||
|
USER: { |
||||
|
INVITE: "budibase:portal_user_invite", |
||||
|
}, |
||||
|
SMTP: { |
||||
|
SAVED: "budibase:smtp_saved", |
||||
|
}, |
||||
|
SSO: { |
||||
|
SAVED: "budibase:sso_saved", |
||||
|
}, |
||||
|
} |
||||
@ -0,0 +1,79 @@ |
|||||
|
import api from "builderStore/api" |
||||
|
import PosthogClient from "./PosthogClient" |
||||
|
import IntercomClient from "./IntercomClient" |
||||
|
import SentryClient from "./SentryClient" |
||||
|
import { Events } from "./constants" |
||||
|
import { auth } from "stores/portal" |
||||
|
import { get } from "svelte/store" |
||||
|
|
||||
|
const posthog = new PosthogClient( |
||||
|
process.env.POSTHOG_TOKEN, |
||||
|
process.env.POSTHOG_URL |
||||
|
) |
||||
|
const sentry = new SentryClient(process.env.SENTRY_DSN) |
||||
|
const intercom = new IntercomClient(process.env.INTERCOM_TOKEN) |
||||
|
|
||||
|
class AnalyticsHub { |
||||
|
constructor() { |
||||
|
this.clients = [posthog, sentry, intercom] |
||||
|
} |
||||
|
|
||||
|
async activate() { |
||||
|
// Setting the analytics env var off in the backend overrides org/tenant settings
|
||||
|
const analyticsStatus = await api.get("/api/analytics") |
||||
|
const json = await analyticsStatus.json() |
||||
|
|
||||
|
// Multitenancy disabled on the backend
|
||||
|
if (!json.enabled) return |
||||
|
|
||||
|
const tenantId = get(auth).tenantId |
||||
|
|
||||
|
if (tenantId) { |
||||
|
const res = await api.get( |
||||
|
`/api/global/configs/public?tenantId=${tenantId}` |
||||
|
) |
||||
|
const orgJson = await res.json() |
||||
|
|
||||
|
// analytics opted out for the tenant
|
||||
|
if (orgJson.config?.analytics === false) return |
||||
|
} |
||||
|
|
||||
|
this.clients.forEach(client => client.init()) |
||||
|
this.enabled = true |
||||
|
} |
||||
|
|
||||
|
identify(id, metadata) { |
||||
|
posthog.identify(id) |
||||
|
if (metadata) { |
||||
|
posthog.updateUser(metadata) |
||||
|
} |
||||
|
sentry.identify(id) |
||||
|
} |
||||
|
|
||||
|
captureException(err) { |
||||
|
sentry.captureException(err) |
||||
|
} |
||||
|
|
||||
|
captureEvent(eventName, props = {}) { |
||||
|
posthog.captureEvent(eventName, props) |
||||
|
intercom.captureEvent(eventName, props) |
||||
|
} |
||||
|
|
||||
|
showChat(user) { |
||||
|
intercom.show(user) |
||||
|
} |
||||
|
|
||||
|
submitFeedback(values) { |
||||
|
posthog.npsFeedback(values) |
||||
|
} |
||||
|
|
||||
|
async logout() { |
||||
|
posthog.logout() |
||||
|
intercom.logout() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const analytics = new AnalyticsHub() |
||||
|
|
||||
|
export { Events } |
||||
|
export default analytics |
||||
File diff suppressed because it is too large
@ -0,0 +1,28 @@ |
|||||
|
version: "3.8" |
||||
|
services: |
||||
|
db: |
||||
|
container_name: postgres-vehicle |
||||
|
image: postgres |
||||
|
restart: always |
||||
|
environment: |
||||
|
POSTGRES_USER: root |
||||
|
POSTGRES_PASSWORD: root |
||||
|
POSTGRES_DB: main |
||||
|
ports: |
||||
|
- "5432:5432" |
||||
|
volumes: |
||||
|
#- pg_data:/var/lib/postgresql/data/ |
||||
|
- ./init.sql:/docker-entrypoint-initdb.d/init.sql |
||||
|
|
||||
|
pgadmin: |
||||
|
container_name: pgadmin |
||||
|
image: dpage/pgadmin4 |
||||
|
restart: always |
||||
|
environment: |
||||
|
PGADMIN_DEFAULT_EMAIL: root@root.com |
||||
|
PGADMIN_DEFAULT_PASSWORD: root |
||||
|
ports: |
||||
|
- "5050:80" |
||||
|
|
||||
|
#volumes: |
||||
|
# pg_data: |
||||
@ -0,0 +1,52 @@ |
|||||
|
SELECT 'CREATE DATABASE main' |
||||
|
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'main')\gexec |
||||
|
CREATE TABLE Vehicles ( |
||||
|
id bigint NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ), |
||||
|
Registration text COLLATE pg_catalog."default", |
||||
|
Make text COLLATE pg_catalog."default", |
||||
|
Model text COLLATE pg_catalog."default", |
||||
|
Colour text COLLATE pg_catalog."default", |
||||
|
Year smallint, |
||||
|
CONSTRAINT Vehicles_pkey PRIMARY KEY (id) |
||||
|
); |
||||
|
|
||||
|
CREATE TABLE ServiceLog ( |
||||
|
id bigint NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ), |
||||
|
Description text COLLATE pg_catalog."default", |
||||
|
VehicleId bigint, |
||||
|
ServiceDate timestamp without time zone, |
||||
|
Category text COLLATE pg_catalog."default", |
||||
|
Mileage bigint, |
||||
|
CONSTRAINT ServiceLog_pkey PRIMARY KEY (id), |
||||
|
CONSTRAINT vehicle_foreign_key FOREIGN KEY (VehicleId) |
||||
|
REFERENCES Vehicles (id) MATCH SIMPLE |
||||
|
ON UPDATE NO ACTION |
||||
|
ON DELETE NO ACTION |
||||
|
); |
||||
|
|
||||
|
INSERT INTO Vehicles (Registration, Make, Model, Colour, Year) |
||||
|
VALUES ('FAZ 9837','Volkswagen','Polo','White',2002); |
||||
|
INSERT INTO Vehicles (Registration, Make, Model, Colour, Year) |
||||
|
VALUES ('JHI 8827','BMW','M3','Black',2013); |
||||
|
INSERT INTO Vehicles (Registration, Make, Model, Colour, Year) |
||||
|
VALUES ('D903PI','Volvo','XC40','Grey',2014); |
||||
|
INSERT INTO Vehicles (Registration, Make, Model, Colour, Year) |
||||
|
VALUES ('YFI002','Volkswagen','Golf','Dark Blue',2018); |
||||
|
INSERT INTO Vehicles (Registration, Make, Model, Colour, Year) |
||||
|
VALUES ('HGT5677','Skoda','Octavia','Graphite',2009); |
||||
|
INSERT INTO Vehicles (Registration, Make, Model, Colour, Year) |
||||
|
VALUES ('PPF9276','Skoda','Octavia','Graphite',2021); |
||||
|
INSERT INTO Vehicles (Registration, Make, Model, Colour, Year) |
||||
|
VALUES ('J893FT','Toyota','Corolla','Red',2015); |
||||
|
INSERT INTO Vehicles (Registration, Make, Model, Colour, Year) |
||||
|
VALUES ('MJK776','Honda','HR-V','Silver',2015); |
||||
|
|
||||
|
|
||||
|
INSERT INTO ServiceLog (Description, VehicleId, ServiceDate, Category, Mileage) |
||||
|
VALUES ('Change front brakes', 1, '2021-05-04', 'Brakes', 20667); |
||||
|
INSERT INTO ServiceLog (Description, VehicleId, ServiceDate, Category, Mileage) |
||||
|
VALUES ('Tyres - full set', 1, '2021-05-04', 'Brakes', 20667); |
||||
|
INSERT INTO ServiceLog (Description, VehicleId, ServiceDate, Category, Mileage) |
||||
|
VALUES ('Engine tune up', 2, '2021-07-14', 'Brakes', 50889); |
||||
|
INSERT INTO ServiceLog (Description, VehicleId, ServiceDate, Category, Mileage) |
||||
|
VALUES ('Replace transmission', 3, '2021-09-26', 'Transmission', 98002); |
||||
@ -0,0 +1,3 @@ |
|||||
|
#!/bin/bash |
||||
|
docker-compose down |
||||
|
docker volume prune -f |
||||
@ -0,0 +1,109 @@ |
|||||
|
const { |
||||
|
ViewNames, |
||||
|
generateMemoryViewID, |
||||
|
getMemoryViewParams, |
||||
|
} = require("../../../db/utils") |
||||
|
const env = require("../../../environment") |
||||
|
|
||||
|
exports.getView = async (db, viewName) => { |
||||
|
if (env.SELF_HOSTED) { |
||||
|
const designDoc = await db.get("_design/database") |
||||
|
return designDoc.views[viewName] |
||||
|
} else { |
||||
|
const viewDoc = await db.get(generateMemoryViewID(viewName)) |
||||
|
return viewDoc.view |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
exports.getViews = async db => { |
||||
|
const response = [] |
||||
|
if (env.SELF_HOSTED) { |
||||
|
const designDoc = await db.get("_design/database") |
||||
|
for (let name of Object.keys(designDoc.views)) { |
||||
|
// Only return custom views, not built ins
|
||||
|
if (Object.values(ViewNames).indexOf(name) !== -1) { |
||||
|
continue |
||||
|
} |
||||
|
response.push({ |
||||
|
name, |
||||
|
...designDoc.views[name], |
||||
|
}) |
||||
|
} |
||||
|
} else { |
||||
|
const views = ( |
||||
|
await db.allDocs( |
||||
|
getMemoryViewParams({ |
||||
|
include_docs: true, |
||||
|
}) |
||||
|
) |
||||
|
).rows.map(row => row.doc) |
||||
|
for (let viewDoc of views) { |
||||
|
response.push({ |
||||
|
name: viewDoc.name, |
||||
|
...viewDoc.view, |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
return response |
||||
|
} |
||||
|
|
||||
|
exports.saveView = async (db, originalName, viewName, viewTemplate) => { |
||||
|
if (env.SELF_HOSTED) { |
||||
|
const designDoc = await db.get("_design/database") |
||||
|
designDoc.views = { |
||||
|
...designDoc.views, |
||||
|
[viewName]: viewTemplate, |
||||
|
} |
||||
|
// view has been renamed
|
||||
|
if (originalName) { |
||||
|
delete designDoc.views[originalName] |
||||
|
} |
||||
|
await db.put(designDoc) |
||||
|
} else { |
||||
|
const id = generateMemoryViewID(viewName) |
||||
|
const originalId = originalName ? generateMemoryViewID(originalName) : null |
||||
|
const viewDoc = { |
||||
|
_id: id, |
||||
|
view: viewTemplate, |
||||
|
name: viewName, |
||||
|
tableId: viewTemplate.meta.tableId, |
||||
|
} |
||||
|
try { |
||||
|
const old = await db.get(id) |
||||
|
if (originalId) { |
||||
|
const originalDoc = await db.get(originalId) |
||||
|
await db.remove(originalDoc._id, originalDoc._rev) |
||||
|
} |
||||
|
if (old && old._rev) { |
||||
|
viewDoc._rev = old._rev |
||||
|
} |
||||
|
} catch (err) { |
||||
|
// didn't exist, just skip
|
||||
|
} |
||||
|
await db.put(viewDoc) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
exports.deleteView = async (db, viewName) => { |
||||
|
if (env.SELF_HOSTED) { |
||||
|
const designDoc = await db.get("_design/database") |
||||
|
const view = designDoc.views[viewName] |
||||
|
delete designDoc.views[viewName] |
||||
|
await db.put(designDoc) |
||||
|
return view |
||||
|
} else { |
||||
|
const id = generateMemoryViewID(viewName) |
||||
|
const viewDoc = await db.get(id) |
||||
|
await db.remove(viewDoc._id, viewDoc._rev) |
||||
|
return viewDoc.view |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
exports.migrateToInMemoryView = async (db, viewName) => { |
||||
|
// delete the view initially
|
||||
|
const designDoc = await db.get("_design/database") |
||||
|
const view = designDoc.views[viewName] |
||||
|
delete designDoc.views[viewName] |
||||
|
await db.put(designDoc) |
||||
|
await exports.saveView(db, null, viewName, view) |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
const PouchDB = require("pouchdb") |
||||
|
const memory = require("pouchdb-adapter-memory") |
||||
|
const newid = require("./newid") |
||||
|
|
||||
|
PouchDB.plugin(memory) |
||||
|
const Pouch = PouchDB.defaults({ |
||||
|
prefix: undefined, |
||||
|
adapter: "memory", |
||||
|
}) |
||||
|
|
||||
|
exports.runView = async (view, calculation, group, data) => { |
||||
|
// use a different ID each time for the DB, make sure they
|
||||
|
// are always unique for each query, don't want overlap
|
||||
|
// which could cause 409s
|
||||
|
const db = new Pouch(newid()) |
||||
|
// write all the docs to the in memory Pouch (remove revs)
|
||||
|
await db.bulkDocs( |
||||
|
data.map(row => ({ |
||||
|
...row, |
||||
|
_rev: undefined, |
||||
|
})) |
||||
|
) |
||||
|
let fn = (doc, emit) => emit(doc._id) |
||||
|
eval("fn = " + view.map.replace("function (doc)", "function (doc, emit)")) |
||||
|
const queryFns = { |
||||
|
meta: view.meta, |
||||
|
map: fn, |
||||
|
} |
||||
|
if (view.reduce) { |
||||
|
queryFns.reduce = view.reduce |
||||
|
} |
||||
|
const response = await db.query(queryFns, { |
||||
|
include_docs: !calculation, |
||||
|
group: !!group, |
||||
|
}) |
||||
|
// need to fix the revs to be totally accurate
|
||||
|
for (let row of response.rows) { |
||||
|
if (!row._rev || !row._id) { |
||||
|
continue |
||||
|
} |
||||
|
const found = data.find(possible => possible._id === row._id) |
||||
|
if (found) { |
||||
|
row._rev = found._rev |
||||
|
} |
||||
|
} |
||||
|
await db.destroy() |
||||
|
return response |
||||
|
} |
||||
Loading…
Reference in new issue