@joystick.js/test-canary 0.0.0-canary.7 → 0.0.0-canary.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/_package.json +8 -1
  2. package/dist/helpers/component/event.js +1 -0
  3. package/dist/helpers/component/index.js +9 -1
  4. package/dist/helpers/databases/index.js +1 -1
  5. package/dist/helpers/databases/mongodb/availableQueryParameters.js +1 -0
  6. package/dist/helpers/databases/mongodb/buildConnectionString.js +1 -0
  7. package/dist/helpers/databases/mongodb/buildQueryParameters.js +1 -0
  8. package/dist/helpers/databases/mongodb/index.js +6 -0
  9. package/dist/helpers/databases/postgresql/addColumnToTable.js +1 -0
  10. package/dist/helpers/databases/postgresql/createAccountsIndexes.js +1 -0
  11. package/dist/helpers/databases/postgresql/createAccountsTables.js +1 -0
  12. package/dist/helpers/databases/postgresql/createDatabase.js +0 -0
  13. package/dist/helpers/databases/postgresql/index.js +11 -0
  14. package/dist/helpers/load/index.js +1 -1
  15. package/dist/helpers/queues/index.js +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/lib/{log.js → CLILog.js} +1 -1
  18. package/dist/lib/generateId.js +1 -0
  19. package/dist/lib/isValidJSONString.js +1 -0
  20. package/dist/lib/loadSettings.js +1 -0
  21. package/dist/lib/serializeQueryParameters.js +1 -0
  22. package/dist/test.js +1 -1
  23. package/package.json +1 -1
  24. package/src/helpers/component/event.js +10 -0
  25. package/src/helpers/component/index.js +103 -1
  26. package/src/helpers/databases/index.js +58 -1
  27. package/src/helpers/databases/mongodb/availableQueryParameters.js +39 -0
  28. package/src/helpers/databases/mongodb/buildConnectionString.js +28 -0
  29. package/src/helpers/databases/mongodb/buildQueryParameters.js +15 -0
  30. package/src/helpers/databases/mongodb/index.js +43 -0
  31. package/src/helpers/databases/postgresql/addColumnToTable.js +3 -0
  32. package/src/helpers/databases/postgresql/createAccountsIndexes.js +29 -0
  33. package/src/helpers/databases/postgresql/createAccountsTables.js +44 -0
  34. package/src/helpers/databases/postgresql/createDatabase.js +0 -0
  35. package/src/helpers/databases/postgresql/index.js +52 -0
  36. package/src/helpers/load/index.js +11 -6
  37. package/src/helpers/queues/index.js +9 -1
  38. package/src/index.js +6 -2
  39. package/src/lib/{log.js → CLILog.js} +1 -1
  40. package/src/lib/generateId.js +73 -0
  41. package/src/lib/isValidJSONString.js +7 -0
  42. package/src/lib/loadSettings.js +90 -0
  43. package/src/lib/serializeQueryParameters.js +5 -0
  44. package/src/test.js +26 -2
package/_package.json CHANGED
@@ -16,6 +16,13 @@
16
16
  "license": "SAUCR",
17
17
  "dependencies": {
18
18
  "ava": "^5.3.1",
19
- "esbuild": "^0.18.17"
19
+ "chalk": "^5.3.0",
20
+ "dayjs": "^1.11.9",
21
+ "esbuild": "^0.18.17",
22
+ "linkedom": "^0.15.1",
23
+ "mongo-uri-tool": "^1.0.1",
24
+ "mongodb": "^5.7.0",
25
+ "node-fetch": "^3.3.2",
26
+ "pg": "^8.11.2"
20
27
  }
21
28
  }
@@ -0,0 +1 @@
1
+ var o=(e="",t="",n={})=>{const p=n?.document?.querySelector("#app")?.querySelector(t),r=new Event(e);return p.dispatchEvent(r),!0};export{o as default};
@@ -1 +1,9 @@
1
- var e={};export{e as default};
1
+ import{parseHTML as m}from"linkedom";import w from"@joystick.js/ui-canary";import d from"node-fetch";import{URL as p,URLSearchParams as u}from"url";import h from"./event.js";import _ from"../load/index.js";const g=()=>{const t=m(`
2
+ <html>
3
+ <head></head>
4
+ <body>
5
+ <div id="app"></div>
6
+ <meta name="csrf" content="joystick_test" />
7
+ </body>
8
+ </html>
9
+ `),{window:c,document:e,Element:s,Event:n,HTMLElement:l}=t;return global.window=c,global.document=e,global.HTMLElement=l,global.Element=s,global.Event=n,global.console={log:console.log,warn:console.warn,error:console.error},t};var T=async(t="",c={})=>{const e=g();window.fetch=d,window.location={origin:`http://localhost:${process.env.PORT}`};const s=new p(`${window?.location?.origin}/api/_test/bootstrap`);s.search=new u({pathToComponent:t});const n=await d(s).then(async o=>o.json());console.log(n),window.joystick={},window.__joystick_data__=n?.data||{},window.__joystick_i18n__=n?.translations||{},window.__joystick_req__={params:{},query:{},context:{user:{}}};const l=await _(t,{default:!0}),r=w.mount(l,c?.props||{},e?.document.querySelector("#app"));return r.isTest=!0,{dom:e,instance:r,test:{data:async(o={})=>r?.data?.refetch(o),render:()=>{const o=r?.renderToHTML(),a=new RegExp("<when>|</when>","g");return o?.wrapped?.replace(a,"")?.replace(/\n|\t/g," ")?.replace(/> *</g,"><")},method:(o="",...a)=>{const i=r?.methods[o];return i?i(...a):null},event:(o="",a="")=>h(o,a,e)}}};export{T as default};
@@ -1 +1 @@
1
- var e={};export{e as default};
1
+ import n from"../../lib/loadSettings.js";import p from"./mongodb/index.js";import i from"./postgresql/index.js";var l=async()=>{const r=(await n({environment:"test"}))?.parsed?.config?.databases?.map(e=>({provider:e?.provider,settings:e}));for(let e=0;e<r?.length;e+=1){const t=r[e],a=r?.filter(s=>s?.provider===s?.provider)?.length>1,o=parseInt(process.env.PORT,10)+10+e;if(t?.provider==="mongodb"){const s=await p(t?.settings,o);process.databases={...process.databases||{},mongodb:a?{...process?.databases?.mongodb||{},[t?.settings?.name||`mongodb_${o}`]:s}:s}}if(t?.provider==="postgresql"){const s=await i(t?.settings,o);process.databases={...process.databases||{},postgresql:a?{...process?.databases?.postgresql||{},[t?.settings?.name||`postgresql_${o}`]:{...s?.pool,query:s?.query}}:{...s?.pool,query:s?.query}}}}return process.databases};export{l as default};
@@ -0,0 +1 @@
1
+ var e=["replicaSet","tls","ssl","tlsCertificateKeyFile","tlsCertificateKeyFilePassword","tlsCAFile","tlsAllowInvalidCertificates","tlsAllowInvalidHostnames","tlsInsecure","connectTimeoutMS","socketTimeoutMS","compressors","zlibCompressionLevel","maxPoolSize","minPoolSize","maxIdleTimeMS","waitQueueMultiple","waitQueueTimeoutMS","w","wtimeoutMS","journal","readConcernLevel","readPreference","maxStalenessSeconds","readPreferenceTags","authSource","authMechanism","authMechanismProperties","gssapiServiceName","localThresholdMS","serverSelectionTimeoutMS","serverSelectionTryOnce","heartbeatFrequencyMS","appName","retryReads","retryWrites","uuidRepresentation"];export{e as default};
@@ -0,0 +1 @@
1
+ import m from"./buildQueryParameters.js";import $ from"../../../lib/serializeQueryParameters.js";var p=(r={})=>{let s="mongodb://";r&&(r.username||r.password)&&(s=`${s}${r.username||r.password?`${r.username||""}${r.username&&r.password?":":""}${r.password||""}@`:""}`),r&&r.hosts&&Array.isArray(r.hosts)&&(s=`${s}${r.hosts.map(e=>`${e.hostname}:${e.port}`).join(",")}`),r&&r.database&&(s=`${s}/${r.database}`);const a=m(r);return Object.keys(a)?.length>0&&(s=`${s}?${$(a)}`),s};export{p as default};
@@ -0,0 +1 @@
1
+ import l from"./availableQueryParameters.js";var f=(r={})=>{const t={};for(let a=0;a<l.length;a+=1){const e=l[a];r&&r[e]&&(t[e]=r[e])}return t};export{f as default};
@@ -0,0 +1,6 @@
1
+ import{MongoClient as s}from"mongodb";import c from"chalk";import a from"mongo-uri-tool";import p from"fs";import l from"./buildConnectionString.js";var b=async(o={},e=2610)=>{const r=o?.connection||{hosts:[{hostname:"127.0.0.1",port:e}],database:"app",replicaSet:`joystick_${e}`},t=l(r),i=a.parseUri(t);try{const n={useNewUrlParser:!0,useUnifiedTopology:!0,ssl:!["development","test"].includes(process.env.NODE_ENV),...o?.options||{}};return o?.options?.ca&&(n.ca=p.readFileSync(o?.options?.ca)),(await s.connect(t,n)).db(i.db)}catch(n){console.warn(c.yellowBright(`
2
+ Failed to connect to MongoDB. Please double-check connection settings and try again.
3
+
4
+ Error from MongoDB:
5
+
6
+ ${c.redBright(n?.message)}`))}};export{b as default};
@@ -0,0 +1 @@
1
+ var a=(e="",r="",s="")=>process.databases._users?.query(`ALTER TABLE ${e} ADD COLUMN ${r} ${s}`);export{a as default};
@@ -0,0 +1 @@
1
+ const a=async(s="",r="",_=[])=>process.databases._users?.query(`CREATE UNIQUE INDEX IF NOT EXISTS ${s} ON ${r}(${_.join(", ")})`),e=async(s="",r="",_=[])=>process.databases._users?.query(`CREATE INDEX IF NOT EXISTS ${s} ON ${r}(${_.join(", ")})`);var o=async()=>{await e("user_by_email","users",["email_address"]),await e("user_by_username","users",["username"]),await e("user_by_user_id","users",["user_id"]),await e("user_session_by_token","users_sessions",["token"]),await e("user_password_reset_token_by_token","users_password_reset_tokens",["token"]),await e("user_password_reset_token_by_user_id_token","users_password_reset_tokens",["user_id","token"]),await e("user_role","users_roles",["role"]),await e("user_roles_by_user_id_role","users_roles",["user_id","role"]),await e("role","roles",["role"]),await a("user_roles","users_roles",["user_id","role"])};export{o as default};
@@ -0,0 +1 @@
1
+ const e=async(t="",s=[])=>process.databases._users?.query(`CREATE TABLE IF NOT EXISTS ${t} (${s.join(", ")})`);var r=async()=>{await e("users",["id bigserial primary key","user_id text","email_address text","password text","username text","language text"]),await e("users_sessions",["id bigserial primary key","user_id text","token text","token_expires_at text"]),await e("users_password_reset_tokens",["id bigserial primary key","user_id text","token text","requested_at text"]),await e("users_verify_email_tokens",["id bigserial primary key","user_id text","token text"]),await e("roles",["id bigserial primary key","role text"]),await e("users_roles",["id bigserial primary key","user_id text","role text"])};export{r as default};
@@ -0,0 +1,11 @@
1
+ import l from"pg";import n from"chalk";import h from"os";const{Pool:i}=l;var u=async(r={},c=2610)=>{const o=r?.connection||{hosts:[{hostname:"127.0.0.1",port:c}],database:"app",username:(h.userInfo()||{}).username||"",password:""};try{const e=o.hosts&&o.hosts[0],a=new i({user:o?.username||"",database:o?.database,password:o?.password||"",host:e?.hostname,port:e?.port,...r?.options||{}});return{pool:a,query:(...t)=>a.query(...t).then(s=>s?.rows||[]).catch(s=>{throw console.log(n.redBright(`
2
+ Failed SQL Statement:
3
+ `)),console.log(t[0]),console.log(`
4
+ `),console.log(n.redBright(`
5
+ Failed Values:
6
+ `)),console.log(t[1]),s})}}catch(e){console.warn(n.yellowBright(`
7
+ Failed to connect to PostgreSQL. Please double-check connection settings and try again.
8
+
9
+ Error from PostgreSQL:
10
+
11
+ ${n.redBright(e?.message)}`))}};export{u as default};
@@ -1 +1 @@
1
- import a from"fs";import e from"../../lib/log.js";var r=async(t="",n={})=>{const s=`.joystick/build/${t?.charAt(0)==="/"?t.substring(1,t.length):t}`;if(!a.existsSync(s))return e(`[test.load] Path at ${s} not found.`,{level:"warning",docs:"https://cheatcode.co/docs/joystick/test/load"}),null;const o=await import(s);return o?.default&&n?.default?o.default:o};export{r as default};
1
+ import n from"fs";import a from"../../lib/CLILog.js";const c=async(o="",s={})=>{const t=await import(`${o}?update=${Date.now()}`);return t?.default&&s?.default?t.default:t};var u=async(o="",s={})=>{const e=o?.charAt(0)==="/"?o.substring(1,o.length):o,t=`${process.cwd()}/.joystick/build/${e}`;return n.existsSync(t)?c(t,s):(a(`[test.load] Path at ${t} not found.`,{level:"warning",docs:"https://cheatcode.co/docs/joystick/test/load"}),null)};export{u as default};
@@ -1 +1 @@
1
- var e={};export{e as default};
1
+ var u={job:(e="",o={})=>{}};export{u as default};
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import o from"./test.js";import r from"./helpers/api/index.js";import m from"./helpers/component/index.js";import t from"./helpers/databases/index.js";import p from"./helpers/email/index.js";import e from"./helpers/queues/index.js";import i from"./helpers/load/index.js";import f from"./helpers/routes/index.js";import a from"./helpers/uploaders/index.js";import s from"./helpers/websockets/index.js";var x={api:r,component:m,databases:t,email:p,load:i,queues:e,routes:f,that:o.that,uploaders:a,websockets:s};export{x as default};
1
+ import o from"./test.js";import r from"./helpers/api/index.js";import t from"./helpers/component/index.js";import e from"./helpers/databases/index.js";import m from"./helpers/email/index.js";import f from"./helpers/queues/index.js";import a from"./helpers/load/index.js";import p from"./helpers/routes/index.js";import i from"./helpers/uploaders/index.js";import s from"./helpers/websockets/index.js";var w={after:o.after,afterEach:o.afterEach,api:r,before:o.before,beforeEach:o.beforeEach,component:t,databases:e,email:m,load:a,queues:f,routes:p,that:o.that,uploaders:i,websockets:s};export{w as default};
@@ -1,4 +1,4 @@
1
- import l from"chalk";import o from"./rainbowRoad.js";var t=(c="",e={})=>{const d={info:"blue",success:"green",warning:"yellowBright",danger:"red"},g={info:"\u2771 Info",success:"\u2771 Ok",warning:"\u2771 Warning",danger:"\u2771 Error"},a=e.level?d[e.level]:"gray",r=e.level?g[e.level]:"Log",$=e.docs||"https://cheatcode.co/docs/joystick";console.log(`
1
+ import l from"chalk";import o from"./rainbowRoad.js";var t=(c="",e={})=>{const g={info:"blue",success:"green",warning:"yellowBright",danger:"red"},d={info:"\u2771 Info",success:"\u2771 Ok",warning:"\u2771 Warning",danger:"\u2771 Error"},a=e.level?g[e.level]:"gray",r=e.level?d[e.level]:"Log",$=e.docs||"https://github.com/cheatcode/joystick";console.log(`
2
2
  ${e.padding||""}${o()}
3
3
  `),console.log(`${e.padding||""}${l[a](`${r}:`)}
4
4
  `),console.log(`${e.padding||""}${l.white(c)}
@@ -0,0 +1 @@
1
+ var l=(e=16)=>{let t=[],a=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"];for(let r=0;r<e;r++)t.push(a[Math.floor(Math.random()*16)]);return t.join("")};export{l as default};
@@ -0,0 +1 @@
1
+ var t=(r="")=>{try{return JSON.parse(r)}catch{return!1}};export{t as default};
@@ -0,0 +1 @@
1
+ import r from"fs";import i from"./CLILog.js";import c from"./isValidJSONString.js";const a=(e="")=>{try{const t=c(e),s=process.env.NODE_ENV==="test"?"test":"start";t||(i(`Failed to parse settings file. Double-check the syntax in your settings.${process.env.NODE_ENV}.json file at the root of your project and rerun joystick ${s}.`,{level:"danger",docs:"https://cheatcode.co/docs/joystick/environment-settings",tools:[{title:"JSON Linter",url:"https://jsonlint.com/"}]}),process.exit(0))}catch(t){throw new Error(`[loadSettings.warnIfInvalidJSONInSettings] ${t.message}`)}},d=(e="")=>{try{return r.readFileSync(e,"utf-8")}catch(t){throw new Error(`[loadSettings.getSettings] ${t.message}`)}},g=(e="")=>{try{const t=r.existsSync(e),s=process.env.NODE_ENV==="test"?"test":"start";t||(i(`A settings file could not be found for this environment (${process.env.NODE_ENV}). Create a settings.${process.env.NODE_ENV}.json file at the root of your project and rerun joystick ${s}.`,{level:"danger",docs:`https://cheatcode.co/docs/joystick/cli/${s}`}),process.exit(0))}catch(t){throw new Error(`[loadSettings.warnIfSettingsNotFound] ${t.message}`)}},l=e=>{try{if(!e)throw new Error("options object is required.");if(!e.environment)throw new Error("options.environment is required.")}catch(t){throw new Error(`[loadSettings.validateOptions] ${t.message}`)}},p=(e,{resolve:t,reject:s})=>{try{l(e);const o=`${process.cwd()}/settings.${e.environment}.json`;g(o);const n=d(o);a(n),process.env.JOYSTICK_SETTINGS=n,t({parsed:JSON.parse(n),unparsed:n})}catch(o){s(`[loadSettings] ${o.message}`)}};var N=e=>new Promise((t,s)=>{p(e,{resolve:t,reject:s})});export{N as default};
@@ -0,0 +1 @@
1
+ var n=(e={})=>Object.entries(e).map(([r,t])=>`${r}=${t}`)?.join("&");export{n as default};
package/dist/test.js CHANGED
@@ -1 +1 @@
1
- import e from"ava";class o{constructor(){}that(t="",r=null){return e(t,r)}}var n=new o;export{n as default};
1
+ import e from"ava";class a{constructor(){}before(r=null){return e.serial.before(r)}beforeEach(r=null){return e.beforeEach(r)}after(r=null){return e.after.always(r)}afterEach(r=null){return e.afterEach.always(r)}that(r="",t=null){return e.serial(r,t)}}var u=new a;export{u as default};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@joystick.js/test-canary",
3
3
  "type": "module",
4
- "version": "0.0.0-canary.7",
4
+ "version": "0.0.0-canary.70",
5
5
  "description": "Isomorphic testing framework for the Joystick JavaScript framework.",
6
6
  "main": "dist/index.js",
7
7
  "scripts": {
@@ -0,0 +1,10 @@
1
+ export default (eventType = '', eventTarget = '', dom = {}) => {
2
+ const app = dom?.document?.querySelector('#app');
3
+
4
+ const target = app?.querySelector(eventTarget);
5
+ const eventToDispatch = new Event(eventType);
6
+
7
+ target.dispatchEvent(eventToDispatch);
8
+
9
+ return true;
10
+ };
@@ -1 +1,103 @@
1
- export default {};
1
+
2
+ import { parseHTML } from 'linkedom';
3
+ import joystick from '@joystick.js/ui-canary';
4
+ import fetch from 'node-fetch';
5
+ import { URL, URLSearchParams } from 'url';
6
+ import event from './event.js';
7
+ import load from "../load/index.js";
8
+
9
+ const loadDOM = () => {
10
+ const dom = parseHTML(`
11
+ <html>
12
+ <head></head>
13
+ <body>
14
+ <div id="app"></div>
15
+ <meta name="csrf" content="joystick_test" />
16
+ </body>
17
+ </html>
18
+ `);
19
+ const { window, document, Element, Event, HTMLElement } = dom;
20
+
21
+ global.window = window;
22
+ global.document = document;
23
+ global.HTMLElement = HTMLElement;
24
+ global.Element = Element;
25
+ global.Event = Event;
26
+ global.console = {
27
+ log: console.log,
28
+ warn: console.warn,
29
+ error: console.error,
30
+ };
31
+
32
+ return dom;
33
+ };
34
+
35
+ export default async (pathToComponent = '', options = {}) => {
36
+ const dom = loadDOM();
37
+
38
+ window.fetch = fetch;
39
+ window.location = {
40
+ origin: `http://localhost:${process.env.PORT}`,
41
+ };
42
+
43
+ const url = new URL(`${window?.location?.origin}/api/_test/bootstrap`);
44
+ url.search = new URLSearchParams({
45
+ pathToComponent,
46
+ });
47
+
48
+ const bootstrap = await fetch(url).then(async (response) => response.json());
49
+
50
+ // TODO: This needs to be more heavily leveraged vis a vis the settings file.
51
+ // Basically, w/o a server we need to be able to pipe in certain data. I *can*
52
+ // technically fetch data internally here and set it (may need some internal
53
+ // endpoints like /api/_test/bootstrap to do it so we can grab translations, etc).
54
+ //
55
+ // One thing we can do with something like test.route() is to auto-map the stuff
56
+ // that's on the window over to the return value from that function. So, we get back
57
+ // something like { html: '', req: {}, data: {}, user: {} }, etc. We want to account
58
+ // for things like having a test user in the config, too, so we're legitimately
59
+ // running requests against the app. Maybe have a fixed test user ID (or array of users)
60
+ // like config.test.users = [{ _id: 'abc123', emailAddress: '', password: '' }].
61
+
62
+ console.log(bootstrap);
63
+
64
+ window.joystick = {};
65
+ window.__joystick_data__ = bootstrap?.data || {};
66
+ window.__joystick_i18n__ = bootstrap?.translations || {};
67
+ window.__joystick_req__ = { params: {}, query: {}, context: { user: {} } };
68
+
69
+ // NOTE: Force default to true as that's the prescribed pattern for
70
+ // Joystick component files.
71
+ const Component = await load(pathToComponent, { default: true });
72
+ const component = joystick.mount(Component, options?.props || {}, dom?.document.querySelector('#app'));
73
+
74
+ // NOTE: Used internally in @joystick.js/ui to control component behavior.
75
+ component.isTest = true;
76
+
77
+ return {
78
+ dom,
79
+ instance: component,
80
+ test: {
81
+ data: async (input = {}) => {
82
+ return component?.data?.refetch(input);
83
+ },
84
+ render: () => {
85
+ const html = component?.renderToHTML();
86
+ const whenRegex = new RegExp('<when>|</when>', 'g');
87
+ return html?.wrapped?.replace(whenRegex, '')?.replace(/\n|\t/g, ' ')?.replace(/> *</g, '><');
88
+ },
89
+ method: (methodName = '', ...methodArgs) => {
90
+ const methodToCall = component?.methods[methodName];
91
+
92
+ if (!methodToCall) {
93
+ return null;
94
+ }
95
+
96
+ return methodToCall(...methodArgs);
97
+ },
98
+ event: (eventType = '', eventTarget = '') => {
99
+ return event(eventType, eventTarget, dom);
100
+ },
101
+ },
102
+ };
103
+ };
@@ -1 +1,58 @@
1
- export default {};
1
+ import loadSettings from "../../lib/loadSettings.js";
2
+ import connectMongoDB from './mongodb/index.js';
3
+ import connectPostgreSQL from './postgresql/index.js';
4
+
5
+ export default async () => {
6
+ const settings = (await loadSettings({ environment: 'test' }))?.parsed;
7
+
8
+ const databases = settings?.config?.databases?.map((database) => {
9
+ return {
10
+ provider: database?.provider,
11
+ settings: database,
12
+ };
13
+ });
14
+
15
+ for (
16
+ let databaseIndex = 0;
17
+ databaseIndex < databases?.length;
18
+ databaseIndex += 1
19
+ ) {
20
+ const database = databases[databaseIndex];
21
+ const hasMultipleOfProvider = (databases?.filter((database) => database?.provider === database?.provider))?.length > 1;
22
+ const databasePort = parseInt(process.env.PORT, 10) + 10 + databaseIndex;
23
+
24
+ if (database?.provider === "mongodb") {
25
+ const mongodb = await connectMongoDB(database?.settings, databasePort);
26
+ process.databases = {
27
+ ...(process.databases || {}),
28
+ mongodb: !hasMultipleOfProvider ? mongodb : {
29
+ ...(process?.databases?.mongodb || {}),
30
+ [database?.settings?.name || `mongodb_${databasePort}`]: mongodb,
31
+ },
32
+ };
33
+ }
34
+
35
+ if (database?.provider === "postgresql") {
36
+ const postgresql = await connectPostgreSQL(
37
+ database?.settings,
38
+ databasePort
39
+ );
40
+
41
+ process.databases = {
42
+ ...(process.databases || {}),
43
+ postgresql: !hasMultipleOfProvider ? {
44
+ ...postgresql?.pool,
45
+ query: postgresql?.query,
46
+ } : {
47
+ ...(process?.databases?.postgresql || {}),
48
+ [database?.settings?.name || `postgresql_${databasePort}`]: {
49
+ ...postgresql?.pool,
50
+ query: postgresql?.query,
51
+ },
52
+ },
53
+ };
54
+ }
55
+ }
56
+
57
+ return process.databases;
58
+ };
@@ -0,0 +1,39 @@
1
+ export default [
2
+ "replicaSet",
3
+ "tls",
4
+ "ssl",
5
+ "tlsCertificateKeyFile",
6
+ "tlsCertificateKeyFilePassword",
7
+ "tlsCAFile",
8
+ "tlsAllowInvalidCertificates",
9
+ "tlsAllowInvalidHostnames",
10
+ "tlsInsecure",
11
+ "connectTimeoutMS",
12
+ "socketTimeoutMS",
13
+ "compressors",
14
+ "zlibCompressionLevel",
15
+ "maxPoolSize",
16
+ "minPoolSize",
17
+ "maxIdleTimeMS",
18
+ "waitQueueMultiple",
19
+ "waitQueueTimeoutMS",
20
+ "w",
21
+ "wtimeoutMS",
22
+ "journal",
23
+ "readConcernLevel",
24
+ "readPreference",
25
+ "maxStalenessSeconds",
26
+ "readPreferenceTags",
27
+ "authSource",
28
+ "authMechanism",
29
+ "authMechanismProperties",
30
+ "gssapiServiceName",
31
+ "localThresholdMS",
32
+ "serverSelectionTimeoutMS",
33
+ "serverSelectionTryOnce",
34
+ "heartbeatFrequencyMS",
35
+ "appName",
36
+ "retryReads",
37
+ "retryWrites",
38
+ "uuidRepresentation"
39
+ ];
@@ -0,0 +1,28 @@
1
+ import buildQueryParameters from "./buildQueryParameters.js";
2
+ import serializeQueryParameters from "../../../lib/serializeQueryParameters.js";
3
+
4
+ export default (connection = {}) => {
5
+ let connectionString = "mongodb://";
6
+
7
+ if (connection && (connection.username || connection.password)) {
8
+ connectionString = `${connectionString}${connection.username || connection.password ? `${connection.username || ""}${!!connection.username && !!connection.password ? ':' : ''}${connection.password || ""}@` : ''}`;
9
+ }
10
+
11
+ if (connection && connection.hosts && Array.isArray(connection.hosts)) {
12
+ connectionString = `${connectionString}${connection.hosts
13
+ .map((host) => `${host.hostname}:${host.port}`)
14
+ .join(",")}`;
15
+ }
16
+
17
+ if (connection && connection.database) {
18
+ connectionString = `${connectionString}/${connection.database}`;
19
+ }
20
+
21
+ const queryParameters = buildQueryParameters(connection);
22
+
23
+ if (Object.keys(queryParameters)?.length > 0 ) {
24
+ connectionString = `${connectionString}?${serializeQueryParameters(queryParameters)}`;
25
+ }
26
+
27
+ return connectionString;
28
+ };
@@ -0,0 +1,15 @@
1
+ import availableQueryParameters from "./availableQueryParameters.js";
2
+
3
+ export default (connection = {}) => {
4
+ const queryParameters = {};
5
+
6
+ for (let i = 0; i < availableQueryParameters.length; i += 1) {
7
+ const availableParameter = availableQueryParameters[i];
8
+
9
+ if (connection && connection[availableParameter]) {
10
+ queryParameters[availableParameter] = connection[availableParameter];
11
+ }
12
+ }
13
+
14
+ return queryParameters;
15
+ };
@@ -0,0 +1,43 @@
1
+ import { MongoClient } from "mongodb";
2
+ import chalk from "chalk";
3
+ import mongoUri from "mongo-uri-tool";
4
+ import fs from 'fs';
5
+ import buildConnectionString from "./buildConnectionString.js";
6
+
7
+ export default async (settings = {}, databasePort = 2610) => {
8
+ const connection = settings?.connection || {
9
+ hosts: [
10
+ // NOTE: By default, expect databases start from 2610 (assuming a PORT of 2600).
11
+ { hostname: "127.0.0.1", port: databasePort },
12
+ ],
13
+ database: "app",
14
+ replicaSet: `joystick_${databasePort}`,
15
+ };
16
+
17
+ const connectionString = buildConnectionString(connection);
18
+ const parsedURI = mongoUri.parseUri(connectionString);
19
+
20
+ try {
21
+ const connectionOptions = {
22
+ useNewUrlParser: true,
23
+ useUnifiedTopology: true,
24
+ ssl: !['development', 'test'].includes(process.env.NODE_ENV),
25
+ ...(settings?.options || {})
26
+ };
27
+
28
+ if (settings?.options?.ca) {
29
+ connectionOptions.ca = fs.readFileSync(settings?.options?.ca);
30
+ }
31
+
32
+ const client = await MongoClient.connect(connectionString, connectionOptions);
33
+ const db = client.db(parsedURI.db);
34
+
35
+ return db;
36
+ } catch (exception) {
37
+ console.warn(
38
+ chalk.yellowBright(
39
+ `\nFailed to connect to MongoDB. Please double-check connection settings and try again.\n\nError from MongoDB:\n\n${chalk.redBright(exception?.message)}`
40
+ ),
41
+ );
42
+ }
43
+ };
@@ -0,0 +1,3 @@
1
+ export default (table = '', columnName = '', columnSchema = '') => {
2
+ return process.databases._users?.query(`ALTER TABLE ${table} ADD COLUMN ${columnName} ${columnSchema}`);
3
+ };
@@ -0,0 +1,29 @@
1
+ const createUniqueIndex = async (indexName = '', tableName = '', tableColumns = []) => {
2
+ return process.databases._users?.query(`CREATE UNIQUE INDEX IF NOT EXISTS ${indexName} ON ${tableName}(${tableColumns.join(', ')})`);
3
+ };
4
+
5
+ const createIndex = async (indexName = '', tableName = '', tableColumns = []) => {
6
+ return process.databases._users?.query(`CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName}(${tableColumns.join(', ')})`);
7
+ };
8
+
9
+ export default async () => {
10
+ // users
11
+ await createIndex('user_by_email', 'users', ['email_address']);
12
+ await createIndex('user_by_username', 'users', ['username']);
13
+ await createIndex('user_by_user_id', 'users', ['user_id']);
14
+
15
+ // users_sessions
16
+ await createIndex('user_session_by_token', 'users_sessions', ['token']);
17
+
18
+ // users_password_reset_tokens
19
+ await createIndex('user_password_reset_token_by_token', 'users_password_reset_tokens', ['token']);
20
+ await createIndex('user_password_reset_token_by_user_id_token', 'users_password_reset_tokens', ['user_id', 'token']);
21
+
22
+ // users_roles
23
+ await createIndex('user_role', 'users_roles', ['role']);
24
+ await createIndex('user_roles_by_user_id_role', 'users_roles', ['user_id', 'role']);
25
+
26
+ // roles
27
+ await createIndex('role', 'roles', ['role']);
28
+ await createUniqueIndex('user_roles', 'users_roles', ['user_id', 'role']);
29
+ };
@@ -0,0 +1,44 @@
1
+ const createTable = async (table = "", tableColumns = []) => {
2
+ return process.databases._users?.query(
3
+ `CREATE TABLE IF NOT EXISTS ${table} (${tableColumns.join(", ")})`
4
+ );
5
+ };
6
+
7
+ export default async () => {
8
+ await createTable("users", [
9
+ "id bigserial primary key",
10
+ "user_id text",
11
+ "email_address text",
12
+ "password text",
13
+ "username text",
14
+ "language text",
15
+ ]);
16
+
17
+ await createTable("users_sessions", [
18
+ "id bigserial primary key",
19
+ "user_id text",
20
+ "token text",
21
+ "token_expires_at text",
22
+ ]);
23
+
24
+ await createTable("users_password_reset_tokens", [
25
+ "id bigserial primary key",
26
+ "user_id text",
27
+ "token text",
28
+ "requested_at text",
29
+ ]);
30
+
31
+ await createTable("users_verify_email_tokens", [
32
+ "id bigserial primary key",
33
+ "user_id text",
34
+ "token text",
35
+ ]);
36
+
37
+ await createTable("roles", ["id bigserial primary key", "role text"]);
38
+
39
+ await createTable("users_roles", [
40
+ "id bigserial primary key",
41
+ "user_id text",
42
+ "role text",
43
+ ]);
44
+ };
@@ -0,0 +1,52 @@
1
+ import postgresql from 'pg';
2
+ import chalk from "chalk";
3
+ import os from 'os';
4
+
5
+ const { Pool } = postgresql;
6
+
7
+ export default async (settings = {}, databasePort = 2610) => {
8
+ const connection = settings?.connection || {
9
+ hosts: [
10
+ { hostname: "127.0.0.1", port: databasePort },
11
+ ],
12
+ database: "app",
13
+ // NOTE: PostgreSQL creates a default superuser based on the OS username.
14
+ username: (os.userInfo() || {}).username || "",
15
+ password: "",
16
+ };
17
+
18
+ try {
19
+ const host = connection.hosts && connection.hosts[0];
20
+ const pool = new Pool({
21
+ user: connection?.username || '',
22
+ database: connection?.database,
23
+ password: connection?.password || '',
24
+ host: host?.hostname,
25
+ port: host?.port,
26
+ ...(settings?.options || {})
27
+ });
28
+
29
+ return {
30
+ pool,
31
+ query: (...args) => {
32
+ return pool.query(...args).then((response) => {
33
+ return response?.rows || [];
34
+ }).catch((error) => {
35
+ console.log(chalk.redBright(`\nFailed SQL Statement:\n`));
36
+ console.log(args[0]);
37
+ console.log(`\n`);
38
+ console.log(chalk.redBright(`\nFailed Values:\n`));
39
+ console.log(args[1]);
40
+
41
+ throw error;
42
+ });
43
+ }
44
+ };
45
+ } catch (exception) {
46
+ console.warn(
47
+ chalk.yellowBright(
48
+ `\nFailed to connect to PostgreSQL. Please double-check connection settings and try again.\n\nError from PostgreSQL:\n\n${chalk.redBright(exception?.message)}`
49
+ ),
50
+ );
51
+ }
52
+ };
@@ -1,13 +1,20 @@
1
1
  import fs from 'fs';
2
- import log from "../../lib/log.js";
2
+ import CLILog from "../../lib/CLILog.js";
3
+
4
+ const uncachedImport = async (path = '', options = {}) => {
5
+ const modulePath = `${path}?update=${Date.now()}`
6
+ const contents = await import(modulePath);
7
+ return (contents?.default && options?.default) ? contents.default : contents;
8
+ };
3
9
 
4
10
  export default async (path = '', options = {}) => {
5
11
  const sanitizedPath = path?.charAt(0) === '/' ? path.substring(1, path.length) : path;
6
- const buildPath = `.joystick/build/${sanitizedPath}`;
12
+ // NOTE: Use timestamp to cache bust on import() below.
13
+ const buildPath = `${process.cwd()}/.joystick/build/${sanitizedPath}`;
7
14
  const pathExists = fs.existsSync(buildPath);
8
15
 
9
16
  if (!pathExists) {
10
- log(`[test.load] Path at ${buildPath} not found.`, {
17
+ CLILog(`[test.load] Path at ${buildPath} not found.`, {
11
18
  level: 'warning',
12
19
  docs: 'https://cheatcode.co/docs/joystick/test/load',
13
20
  });
@@ -15,7 +22,5 @@ export default async (path = '', options = {}) => {
15
22
  return null;
16
23
  }
17
24
 
18
- const contents = await import(buildPath);
19
-
20
- return (contents?.default && options?.default) ? contents.default : contents;
25
+ return uncachedImport(buildPath, options);
21
26
  };
@@ -1 +1,9 @@
1
- export default {};
1
+ export default {
2
+ job: (queueName = '', job = {}) => {
3
+ // TODO: Add a job of name to queueName.
4
+ // TODO: Run the job immediately.
5
+ // TODO: Return result of job.
6
+ // TODO: In test, either verify that response is as expected, or, test for known
7
+ // side-effects of the job in database.
8
+ },
9
+ };
package/src/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import _test from './test.js';
1
+ import test from './test.js';
2
2
  import api from './helpers/api/index.js';
3
3
  import component from './helpers/component/index.js';
4
4
  import databases from './helpers/databases/index.js';
@@ -10,14 +10,18 @@ import uploaders from './helpers/uploaders/index.js';
10
10
  import websockets from './helpers/websockets/index.js';
11
11
 
12
12
  export default {
13
+ after: test.after,
14
+ afterEach: test.afterEach,
13
15
  api,
16
+ before: test.before,
17
+ beforeEach: test.beforeEach,
14
18
  component,
15
19
  databases,
16
20
  email,
17
21
  load,
18
22
  queues,
19
23
  routes,
20
- that: _test.that,
24
+ that: test.that,
21
25
  uploaders,
22
26
  websockets,
23
27
  };
@@ -18,7 +18,7 @@ export default (message = '', options = {}) => {
18
18
 
19
19
  const color = options.level ? colors[options.level] : 'gray';
20
20
  const title = options.level ? titles[options.level] : 'Log';
21
- const docs = options.docs || 'https://cheatcode.co/docs/joystick';
21
+ const docs = options.docs || 'https://github.com/cheatcode/joystick';
22
22
 
23
23
  console.log(`\n${(options.padding || '')}${rainbowRoad()}\n`);
24
24
  console.log(`${(options.padding || '')}${chalk[color](`${title}:`)}\n`)
@@ -0,0 +1,73 @@
1
+ export default (length = 16) => {
2
+ let result = [];
3
+ let character = [
4
+ "0",
5
+ "1",
6
+ "2",
7
+ "3",
8
+ "4",
9
+ "5",
10
+ "6",
11
+ "7",
12
+ "8",
13
+ "9",
14
+ "a",
15
+ "b",
16
+ "c",
17
+ "d",
18
+ "e",
19
+ "f",
20
+ "g",
21
+ "h",
22
+ "i",
23
+ "j",
24
+ "k",
25
+ "l",
26
+ "m",
27
+ "n",
28
+ "o",
29
+ "p",
30
+ "q",
31
+ "r",
32
+ "s",
33
+ "t",
34
+ "u",
35
+ "v",
36
+ "w",
37
+ "x",
38
+ "y",
39
+ "z",
40
+ "A",
41
+ "B",
42
+ "C",
43
+ "D",
44
+ "E",
45
+ "F",
46
+ "G",
47
+ "H",
48
+ "I",
49
+ "J",
50
+ "K",
51
+ "L",
52
+ "M",
53
+ "N",
54
+ "O",
55
+ "P",
56
+ "Q",
57
+ "R",
58
+ "S",
59
+ "T",
60
+ "U",
61
+ "V",
62
+ "W",
63
+ "X",
64
+ "Y",
65
+ "Z",
66
+ ];
67
+
68
+ for (let n = 0; n < length; n++) {
69
+ result.push(character[Math.floor(Math.random() * 16)]);
70
+ }
71
+
72
+ return result.join("");
73
+ };
@@ -0,0 +1,7 @@
1
+ export default (JSONString = "") => {
2
+ try {
3
+ return JSON.parse(JSONString);
4
+ } catch (error) {
5
+ return false;
6
+ }
7
+ };
@@ -0,0 +1,90 @@
1
+ /* eslint-disable consistent-return */
2
+
3
+ import fs from 'fs';
4
+ import CLILog from "./CLILog.js";
5
+ import isValidJSONString from "./isValidJSONString.js";
6
+
7
+ const warnIfInvalidJSONInSettings = (settings = '') => {
8
+ try {
9
+ const isValidJSON = isValidJSONString(settings);
10
+ const context = process.env.NODE_ENV === 'test' ? 'test' : 'start';
11
+
12
+ if (!isValidJSON) {
13
+ CLILog(
14
+ `Failed to parse settings file. Double-check the syntax in your settings.${process.env.NODE_ENV}.json file at the root of your project and rerun joystick ${context}.`,
15
+ {
16
+ level: "danger",
17
+ docs: `https://cheatcode.co/docs/joystick/environment-settings`,
18
+ tools: [{ title: "JSON Linter", url: "https://jsonlint.com/" }],
19
+ }
20
+ );
21
+
22
+ process.exit(0);
23
+ }
24
+ } catch (exception) {
25
+ throw new Error(`[loadSettings.warnIfInvalidJSONInSettings] ${exception.message}`);
26
+ }
27
+ };
28
+
29
+ const getSettings = (settingsPath = '') => {
30
+ try {
31
+ return fs.readFileSync(settingsPath, 'utf-8');
32
+ } catch (exception) {
33
+ throw new Error(`[loadSettings.getSettings] ${exception.message}`);
34
+ }
35
+ };
36
+
37
+ const warnIfSettingsNotFound = (settingsPath = '') => {
38
+ try {
39
+ const hasSettingsFile = fs.existsSync(settingsPath);
40
+ const context = process.env.NODE_ENV === 'test' ? 'test' : 'start';
41
+
42
+ if (!hasSettingsFile) {
43
+ CLILog(
44
+ `A settings file could not be found for this environment (${process.env.NODE_ENV}). Create a settings.${process.env.NODE_ENV}.json file at the root of your project and rerun joystick ${context}.`,
45
+ {
46
+ level: "danger",
47
+ docs: `https://cheatcode.co/docs/joystick/cli/${context}`,
48
+ }
49
+ );
50
+
51
+ process.exit(0);
52
+ }
53
+ } catch (exception) {
54
+ throw new Error(`[loadSettings.warnIfSettingsNotFound] ${exception.message}`);
55
+ }
56
+ };
57
+
58
+ const validateOptions = (options) => {
59
+ try {
60
+ if (!options) throw new Error('options object is required.');
61
+ if (!options.environment) throw new Error('options.environment is required.');
62
+ } catch (exception) {
63
+ throw new Error(`[loadSettings.validateOptions] ${exception.message}`);
64
+ }
65
+ };
66
+
67
+ const loadSettings = (options, { resolve, reject }) => {
68
+ try {
69
+ validateOptions(options);
70
+
71
+ const settingsPath = `${process.cwd()}/settings.${options.environment}.json`;
72
+ warnIfSettingsNotFound(settingsPath);
73
+ const settings = getSettings(settingsPath);
74
+ warnIfInvalidJSONInSettings(settings);
75
+
76
+ process.env.JOYSTICK_SETTINGS = settings;
77
+
78
+ resolve({
79
+ parsed: JSON.parse(settings),
80
+ unparsed: settings,
81
+ });
82
+ } catch (exception) {
83
+ reject(`[loadSettings] ${exception.message}`);
84
+ }
85
+ };
86
+
87
+ export default (options) =>
88
+ new Promise((resolve, reject) => {
89
+ loadSettings(options, { resolve, reject });
90
+ });
@@ -0,0 +1,5 @@
1
+ export default (queryParameters = {}) => {
2
+ return Object.entries(queryParameters).map(([key, value]) => {
3
+ return `${key}=${value}`;
4
+ })?.join('&');
5
+ };
package/src/test.js CHANGED
@@ -4,9 +4,33 @@ class Test {
4
4
  constructor() {
5
5
 
6
6
  }
7
-
7
+
8
+ before(callback = null) {
9
+ // NOTE: Prefer serial before to async to align better with
10
+ // expectations and avoid confusion.
11
+ return test.serial.before(callback);
12
+ }
13
+
14
+ beforeEach(callback = null) {
15
+ return test.beforeEach(callback);
16
+ }
17
+
18
+ after(callback = null) {
19
+ // NOTE: Prefer after always to guarantee cleanup and avoid
20
+ // messy test suites that may or may not cleanup due to failures.
21
+ return test.after.always(callback);
22
+ }
23
+
24
+ afterEach(callback = null) {
25
+ // NOTE: Prefer afterEach always to guarantee cleanup and avoid
26
+ // messy test suites that may or may not cleanup due to failures.
27
+ return test.afterEach.always(callback);
28
+ }
29
+
8
30
  that(description = '', callback = null) {
9
- return test(description, callback);
31
+ // NOTE: Always run serial so we don't have collisions on component instances
32
+ // for DOM tests.
33
+ return test.serial(description, callback);
10
34
  }
11
35
  }
12
36