@joystick.js/test-canary 0.0.0-canary.9 → 0.0.0-canary.90

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 (46) hide show
  1. package/_package.json +8 -1
  2. package/dist/helpers/databases/index.js +1 -1
  3. package/dist/helpers/databases/mongodb/availableQueryParameters.js +1 -0
  4. package/dist/helpers/databases/mongodb/buildConnectionString.js +1 -0
  5. package/dist/helpers/databases/mongodb/buildQueryParameters.js +1 -0
  6. package/dist/helpers/databases/mongodb/index.js +6 -0
  7. package/dist/helpers/databases/postgresql/addColumnToTable.js +1 -0
  8. package/dist/helpers/databases/postgresql/createAccountsIndexes.js +1 -0
  9. package/dist/helpers/databases/postgresql/createAccountsTables.js +1 -0
  10. package/dist/helpers/databases/postgresql/createDatabase.js +0 -0
  11. package/dist/helpers/databases/postgresql/index.js +11 -0
  12. package/dist/helpers/load/index.js +1 -1
  13. package/dist/helpers/queues/index.js +1 -1
  14. package/dist/helpers/render/event.js +1 -0
  15. package/dist/helpers/render/index.js +9 -0
  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/databases/index.js +58 -1
  25. package/src/helpers/databases/mongodb/availableQueryParameters.js +39 -0
  26. package/src/helpers/databases/mongodb/buildConnectionString.js +28 -0
  27. package/src/helpers/databases/mongodb/buildQueryParameters.js +15 -0
  28. package/src/helpers/databases/mongodb/index.js +43 -0
  29. package/src/helpers/databases/postgresql/addColumnToTable.js +3 -0
  30. package/src/helpers/databases/postgresql/createAccountsIndexes.js +29 -0
  31. package/src/helpers/databases/postgresql/createAccountsTables.js +44 -0
  32. package/src/helpers/databases/postgresql/createDatabase.js +0 -0
  33. package/src/helpers/databases/postgresql/index.js +52 -0
  34. package/src/helpers/load/index.js +11 -6
  35. package/src/helpers/queues/index.js +9 -1
  36. package/src/helpers/render/event.js +10 -0
  37. package/src/helpers/render/index.js +92 -0
  38. package/src/index.js +11 -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 +66 -3
  45. package/dist/helpers/component/index.js +0 -1
  46. package/src/helpers/component/index.js +0 -1
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
  }
@@ -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};
@@ -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};
@@ -0,0 +1,9 @@
1
+ import{parseHTML as l}from"linkedom";import d from"@joystick.js/ui-canary";import i from"node-fetch";import{URL as w,URLSearchParams as m}from"url";import p from"./event.js";import h from"../load/index.js";const u=async(e="")=>{const r=new w(`${window?.location?.origin}/api/_test/bootstrap`);r.search=new m({pathToComponent:e});const o=await i(r).then(async a=>a.json());window.joystick={},window.joystick.settings={},window.__joystick_data__=o?.data||{},window.__joystick_i18n__=o?.translations||{},window.__joystick_req__=o?.req},_=()=>{const e=l(`
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:r,document:o,Element:a,Event:n,HTMLElement:t}=e;return global.window=r,global.document=o,global.HTMLElement=t,global.Element=a,global.Event=n,global.console={log:console.log,warn:console.warn,error:console.error},e};var k=async(e="",r={})=>{const o=_();window.fetch=i,window.location={origin:`http://localhost:${process.env.PORT}`},await u(e);const a=await h(e,{default:!0}),n=d.mount(a,r?.props||{},o?.document.querySelector("#app"));return n.isTest=!0,{dom:o,instance:n,test:{data:async(t={})=>n?.data?.refetch(t),renderToHTML:()=>{const t=n?.renderToHTML(),s=new RegExp("<when>|</when>","g");return t?.wrapped?.replace(s,"")?.replace(/\n|\t/g," ")?.replace(/> *</g,"><")},method:(t="",...s)=>{const c=n?.methods[t];return c?c(...s):null},event:(t="",s="")=>p(t,s,o)}}};export{k 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 r from"./test.js";import e from"./helpers/api/index.js";import o from"./helpers/render/index.js";import t from"./helpers/databases/index.js";import m from"./helpers/email/index.js";import a from"./helpers/queues/index.js";import f 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 k={accounts:{signup:r.signupUser,delete:r.deleteUser},after:r.after,afterEach:r.afterEach,api:e,before:r.before,beforeEach:r.beforeEach,render:o,databases:t,email:m,load:f,queues:a,routes:p,that:r.that,uploaders:i,websockets:s};export{k 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 t from"ava";import o from"node-fetch";let a=[];t.serial.after.always(async()=>{for(let s=0;s<a?.length;s+=1){const e=a[s];await(void 0).deleteUser(e?._id||e?.user_id)}});class i{constructor(){}async signupUser(e=[]){for(let r=0;r<e?.length;r+=1){const n=e[r],l=await o(`http://localhost:${process.env.PORT}/api/_test/accounts/signup`,{method:"POST",mode:"cors",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)}).then(c=>c.json());a.push(l)}}deleteUser(e=""){return o(`http://localhost:${process.env.PORT}/api/_test/accounts`,{method:"DELETE",mode:"cors",headers:{"Content-Type":"application/json"},body:JSON.stringify({userId:e})})}before(e=null){return t.serial.before(e)}beforeEach(e=null){return t.beforeEach(e)}after(e=null){return t.after.always(e)}afterEach(e=null){return t.afterEach.always(e)}that(e="",r=null){return t.serial(e,r)}}var p=new i;export{p 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.9",
4
+ "version": "0.0.0-canary.90",
5
5
  "description": "Isomorphic testing framework for the Joystick JavaScript framework.",
6
6
  "main": "dist/index.js",
7
7
  "scripts": {
@@ -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
+ };
@@ -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
+ };
@@ -0,0 +1,92 @@
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 bootstrapWindow = async (pathToComponent = '') => {
10
+ const url = new URL(`${window?.location?.origin}/api/_test/bootstrap`);
11
+ url.search = new URLSearchParams({ pathToComponent });
12
+
13
+ const bootstrap = await fetch(url).then(async (response) => response.json());
14
+
15
+ window.joystick = {};
16
+ window.joystick.settings = {}; // TODO
17
+ window.__joystick_data__ = bootstrap?.data || {};
18
+ window.__joystick_i18n__ = bootstrap?.translations || {};
19
+ window.__joystick_req__ = bootstrap?.req; // TODO
20
+ };
21
+
22
+ const loadDOM = () => {
23
+ const dom = parseHTML(`
24
+ <html>
25
+ <head></head>
26
+ <body>
27
+ <div id="app"></div>
28
+ <meta name="csrf" content="joystick_test" />
29
+ </body>
30
+ </html>
31
+ `);
32
+ const { window, document, Element, Event, HTMLElement } = dom;
33
+
34
+ global.window = window;
35
+ global.document = document;
36
+ global.HTMLElement = HTMLElement;
37
+ global.Element = Element;
38
+ global.Event = Event;
39
+ global.console = {
40
+ log: console.log,
41
+ warn: console.warn,
42
+ error: console.error,
43
+ };
44
+
45
+ return dom;
46
+ };
47
+
48
+ export default async (pathToComponent = '', options = {}) => {
49
+ const dom = loadDOM();
50
+
51
+ window.fetch = fetch;
52
+ window.location = {
53
+ origin: `http://localhost:${process.env.PORT}`,
54
+ };
55
+
56
+ await bootstrapWindow(pathToComponent);
57
+
58
+ // NOTE: Force default to true as that's the prescribed pattern for
59
+ // Joystick component files.
60
+ const Component = await load(pathToComponent, { default: true });
61
+ const component = joystick.mount(Component, options?.props || {}, dom?.document.querySelector('#app'));
62
+
63
+ // NOTE: Used internally in @joystick.js/ui to control component behavior.
64
+ component.isTest = true;
65
+
66
+ return {
67
+ dom,
68
+ instance: component,
69
+ test: {
70
+ data: async (input = {}) => {
71
+ return component?.data?.refetch(input);
72
+ },
73
+ renderToHTML: () => {
74
+ const html = component?.renderToHTML();
75
+ const whenRegex = new RegExp('<when>|</when>', 'g');
76
+ return html?.wrapped?.replace(whenRegex, '')?.replace(/\n|\t/g, ' ')?.replace(/> *</g, '><');
77
+ },
78
+ method: (methodName = '', ...methodArgs) => {
79
+ const methodToCall = component?.methods[methodName];
80
+
81
+ if (!methodToCall) {
82
+ return null;
83
+ }
84
+
85
+ return methodToCall(...methodArgs);
86
+ },
87
+ event: (eventType = '', eventTarget = '') => {
88
+ return event(eventType, eventTarget, dom);
89
+ },
90
+ },
91
+ };
92
+ };
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import test from './test.js';
2
2
  import api from './helpers/api/index.js';
3
- import component from './helpers/component/index.js';
3
+ import render from './helpers/render/index.js';
4
4
  import databases from './helpers/databases/index.js';
5
5
  import email from './helpers/email/index.js';
6
6
  import queues from './helpers/queues/index.js';
@@ -10,8 +10,17 @@ import uploaders from './helpers/uploaders/index.js';
10
10
  import websockets from './helpers/websockets/index.js';
11
11
 
12
12
  export default {
13
+ accounts: {
14
+ // login: test.login,
15
+ signup: test.signupUser,
16
+ delete: test.deleteUser,
17
+ },
18
+ after: test.after,
19
+ afterEach: test.afterEach,
13
20
  api,
14
- component,
21
+ before: test.before,
22
+ beforeEach: test.beforeEach,
23
+ render,
15
24
  databases,
16
25
  email,
17
26
  load,
@@ -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
@@ -1,12 +1,75 @@
1
1
  import test from 'ava';
2
+ import fetch from 'node-fetch';
2
3
 
3
- class Test {
4
- constructor() {
4
+ let users = [];
5
+
6
+ test.serial.after.always(async () => {
7
+ // NOTE: Nuke users who were created as part of the test run to avoid
8
+ // data collisions in other tests/suites.
5
9
 
10
+ for (let i = 0; i < users?.length; i += 1) {
11
+ const user = users[i];
12
+ await this.deleteUser(user?._id || user?.user_id);
6
13
  }
14
+ });
7
15
 
16
+ class Test {
17
+ constructor() {
18
+ }
19
+
20
+ async signupUser(usersToSignup = []) {
21
+ for (let i = 0; i < usersToSignup?.length; i += 1) {
22
+ const userToSignup = usersToSignup[i];
23
+ const user = await fetch(`http://localhost:${process.env.PORT}/api/_test/accounts/signup`, {
24
+ method: 'POST',
25
+ mode: "cors",
26
+ headers: {
27
+ "Content-Type": "application/json",
28
+ },
29
+ body: JSON.stringify(userToSignup)
30
+ }).then((response) => response.json());
31
+
32
+ users.push(user);
33
+ }
34
+ }
35
+
36
+ deleteUser(userId = '') {
37
+ return fetch(`http://localhost:${process.env.PORT}/api/_test/accounts`, {
38
+ method: 'DELETE',
39
+ mode: "cors",
40
+ headers: {
41
+ "Content-Type": "application/json",
42
+ },
43
+ body: JSON.stringify({ userId })
44
+ })
45
+ }
46
+
47
+ before(callback = null) {
48
+ // NOTE: Prefer serial before to async to align better with
49
+ // expectations and avoid confusion.
50
+ return test.serial.before(callback);
51
+ }
52
+
53
+ beforeEach(callback = null) {
54
+ return test.beforeEach(callback);
55
+ }
56
+
57
+ after(callback = null) {
58
+ // NOTE: Prefer after always to guarantee cleanup and avoid
59
+ // messy test suites that may or may not cleanup due to failures.
60
+ return test.after.always(callback);
61
+ }
62
+
63
+ afterEach(callback = null) {
64
+ // NOTE: Prefer afterEach always to guarantee cleanup and avoid
65
+ // messy test suites that may or may not cleanup due to failures.
66
+ return test.afterEach.always(callback);
67
+ }
68
+
8
69
  that(description = '', callback = null) {
9
- return test(description, callback);
70
+ // NOTE: Always run serial so we don't have collisions on component instances
71
+ // for DOM tests.
72
+ return test.serial(description, callback);
10
73
  }
11
74
  }
12
75
 
@@ -1 +0,0 @@
1
- var e={};export{e as default};
@@ -1 +0,0 @@
1
- export default {};