rudderstash 0.1.0 → 0.1.1
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.
- package/README.md +29 -4
- package/dist/rudderstash.js +10 -6
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -39,15 +39,40 @@ Modify.
|
|
|
39
39
|
rudderstash push
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
## Testing Transformations
|
|
43
|
+
|
|
44
|
+
Testing is simple. Add a `MyTransformation.tests.json` file corresponding to the transformation you'd like to test containing any number of tests.
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
[
|
|
48
|
+
{
|
|
49
|
+
"name": "My first test",
|
|
50
|
+
"input": {
|
|
51
|
+
"ip": "127.0.0.1",
|
|
52
|
+
"browser": "Chrome",
|
|
53
|
+
},
|
|
54
|
+
"expected": {
|
|
55
|
+
"ip": "127.0.X.X",
|
|
56
|
+
"browser": "Chrome",
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Now run all tests against matching transformers like so `rudderstash test`.
|
|
63
|
+
|
|
64
|
+
*Tip: You can use tests to debug your transformers with anything from good old `console.log` to attaching full fledged debuggers via the `debugger` statement!*
|
|
65
|
+
|
|
42
66
|
## Roadmap
|
|
43
67
|
|
|
44
|
-
- [x] Basic synchronization
|
|
45
|
-
- [ ] Handle renames
|
|
46
68
|
- [ ] TypeScript support
|
|
47
|
-
- [ ] Polish a lot
|
|
48
|
-
- [ ] Testing
|
|
49
69
|
- [ ] Support revisions
|
|
50
70
|
- [ ] Git integration
|
|
71
|
+
- [ ] Add colors
|
|
72
|
+
- [ ] Polish a lot
|
|
73
|
+
- [ ] Test metadata support
|
|
74
|
+
- [ ] Stage or allow partial pushes
|
|
75
|
+
- [ ] Support non-deterministic tests
|
|
51
76
|
|
|
52
77
|
## Support & License
|
|
53
78
|
|
package/dist/rudderstash.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";import{writeFile as h,readFile as g}from"fs/promises";import
|
|
3
|
-
`),console.info("Upstream:"),!Object.keys(
|
|
4
|
-
`);else for(const
|
|
5
|
-
Local:`),!Object.keys(
|
|
6
|
-
`);else for(const
|
|
7
|
-
`)+". Resolve by leaving only one.");continue}const o=await g(
|
|
2
|
+
"use strict";import{writeFile as h,readdir as D,readFile as g}from"fs/promises";import"colors";import I from"dotenv";import P from"yargs";import{hideBin as k}from"yargs/helpers";import{diffJson as J}from"diff";import{existsSync as j}from"fs";import{writeFile as $,readdir as O,readFile as l}from"fs/promises";import{createHash as R}from"crypto";var S="0.1.1",E="[e0e30bc@trunk; built: 2025-12-20T15:32:51Z]",y=class{headers={};endpoint;constructor(c){this.endpoint=c.RUDDERSTACK_API_ENDPOINT,this.headers={"User-Agent":`rudderstash/${S}`,"Content-Type":"application/json",Authorization:"Basic "+Buffer.from(`${c.RUDDERSTACK_API_USER}:${c.RUDDERSTACK_API_TOKEN}`).toString("base64")}}async get(c){return(await fetch(this.endpoint+c,{headers:this.headers})).json()}async post(c,a){return(await fetch(this.endpoint+c,{method:"POST",headers:this.headers,body:JSON.stringify(a)})).json()}},b=class N{id;versionId;createdAt;updatedAt;name;description;code="";path;constructor(a={}){this.id=a.id,this.versionId=a.versionId,this.createdAt=a.createdAt,this.updatedAt=new Date(a.updatedAt),this.name=a.name}static async get(a){const r=await O(".");for(const s of r.filter(e=>e.endsWith(".transformation.js"))){const e=await l(s,"utf-8");if(!j(`${s}on`))continue;const t=JSON.parse(await l(`${s}on`,"utf-8"));if(t.id===a){const o=new N(t);return o.code=e,o}}return null}hash(){return R("sha1").update(JSON.stringify({id:this.id,code:this.code})).digest("hex")}},u=class _{transformations=[];api;constructor(a){this.api=new y(a)}static async read(a){const r=new _(a),s=await O(".");for(const e of s.filter(t=>t.endsWith(".transformation.js"))){if(!j(`${e}on`)){const n=new b;n.name=e.split(".transformation.js").at(0),n.code=await l(e,"utf-8"),n.path=e,r.transformations.push(n);continue}const t=JSON.parse(await l(`${e}on`,"utf-8")),o=await b.get(t.id);o&&(o.path=e,r.transformations.push(o))}return await r.fetch(),r}async fetch(){j(".ruddercache")||await $(".ruddercache","{}");const a=JSON.parse(await l(".ruddercache","utf8"));a.transformations=(await this.api.get("/transformations")).transformations,a.transformations_fetched_at=Date.now(),await $(".ruddercache",JSON.stringify(a))}async list(){j(".ruddercache")||await $(".ruddercache","{}");let a=JSON.parse(await l(".ruddercache","utf8"));a.transformations===void 0&&(this.fetch(),a=JSON.parse(await l(".ruddercache","utf8")));const r=a.transformations,s={},e={};for(const t of r){const o=t.id;s[o]=new b(t),s[o].code=t.code}for(const t of this.transformations){const o=t.id??crypto.randomUUID();t.id=o,e[o]||(e[o]=[]),e[o].push(t)}return{transformations_fetched_at:a.transformations_fetched_at,transformations:[s,e]}}},d={};I.config({quiet:!0,processEnv:d,override:!1}),Object.entries(d).forEach(([c,a])=>{c in process.env&&(d[c]=process.env[c])});var T=await P(k(process.argv)).parse(),[U,...H]=T._;switch(U){case"version":{console.info(`rudderstash ${S} ${E}`);break}case"status":{const a=await(await u.read(d)).list(),[r,s]=a.transformations;if(console.info(`Last fetched ${new Date(a.transformations_fetched_at)}
|
|
3
|
+
`),console.info("Upstream:"),!Object.keys(r).length)console.info(` (none)
|
|
4
|
+
`);else for(const e of Object.values(r)){let t=" ";const o=[];if(s[e.id]===void 0)t="+";else{const n=(s[e.id]??[]).reduce((f,p)=>f||p.hash()!==e.hash(),!1),i=(s[e.id]??[]).reduce((f,p)=>f||p.name!==e.name||p.description!==e.description,!1);n&&(t="~"),i&&(t="~",o.push("metadata changed")),!n&&!i&&o.push("no changes")}console.info(` ${t} ${e.hash().substring(0,7)} ${e.name}`+(o.length?` (${o.join(", ")})`:""))}if(console.info(`
|
|
5
|
+
Local:`),!Object.keys(s).length)console.info(` (none)
|
|
6
|
+
`);else for(const e of Object.values(s))for(const t of e){let o=" ",n=[];e.filter(f=>f.id===t.id).length>1&&(o="!",n.push("duplicate")),r[t.id]===void 0&&(o="+"),console.info(` ${o} ${t.hash().substring(0,7)} ${t.name} (${t.path})`+(n.length?` (${n.join(", ")})`:""))}break}case"pull":{const a=await(await u.read(d)).list(),[r,s]=a.transformations;if(Object.keys(r).length<1){console.info("No transformations upstream, nothing to pull.");break}for(const e of Object.values(r))if(s[e.id])for(const t of Object.values(s[e.id])){const o=t.path;console.info(`< Pulling new transformation ${e.name} to ${o}...`),await h(o,e.code),console.info(`< Saving metadata to ${o}on.`),delete e.code,await h(o+"on",JSON.stringify(e,null,2))}else{const t=e.name.split(" ").map(o=>o[0].toUpperCase()+o.substring(1)).join("")+".transformation.js";console.info(`< Pulling new transformation ${e.name} to ${t}...`),await h(t,e.code),console.info(`< Saving metadata to ${t}on.`),delete e.code,await h(t+"on",JSON.stringify(e,null,2))}break}case"push":{const a=await(await u.read(d)).list(),[r,s]=a.transformations;if(Object.values(s).length<1){console.info("No local transformations, nothing to push.");break}const e=new y(d);for(const t of Object.values(s)){if(Object.values(t).length>1){console.warn("Conflict! The following local transformations have the same ID: "+Object.values(t).map(f=>f.path).join(`
|
|
7
|
+
`)+". Resolve by leaving only one.".red);continue}const o=await g(t[0].path,"utf-8");let n={};t[0].versionId===void 0?(n.id="",n.name=t[0].path.split(".transformation.js").at(0),n.description=t[0].path):n=JSON.parse(await g(`${t[0].path}on`,"utf-8")),console.info(`> Pushing ${o.length} bytes of code to ${n.name} transformation...`);const i=await e.post(`/transformations/${n.id}?publish=true`,{code:o,name:n.name,language:"javascript",description:n.description});if(i.error!==void 0){console.error(i.error);continue}delete i.code,await h(`${t[0].path}on`,JSON.stringify({...n,...i},null,2)),console.info(`> Published with version ${i.versionId}.`),console.info("< Metadata updated.")}break}case"diff":{const a=await(await u.read(d)).list()}case"revert":break;case"test":{const a=await(await u.read(d)).list(),[r,s]=a.transformations,e=(await D(".")).filter(t=>t.endsWith(".tests.json"));e.length<1&&(console.error("No tests to run [*.tests.json].".red),process.exit(-1));for(const t of e)Object.values(s).flat().find(n=>n.path===t.replace(".tests.json",".transformation.js"))===void 0&&(console.error(`${t} has no matching ${t.replace(".tests.json",".transformation.js")} file.`.red),process.exit(-1));process.removeAllListeners("warning");for(const t of e){const o=Object.values(s).flat().find(i=>i.path===t.replace(".tests.json",".transformation.js"));console.info(`${o.name}:`);const n=JSON.parse(await g(t,"utf8"));for(const i of n){let f=" ERR";const A=await import(`file://${process.cwd()}/${o.path}`),w=J(i.expected,A.transformEvent(i.input));switch(f="FAIL",w.length===1&&!w[0].added&&!w[0].removed&&(f="PASS"),f){case"FAIL":console.warn(` ${f} - ${t.split(".json").at(0)}: ${i.name} `.red);for(const m of w){if(!m.added&&!m.removed){console.warn();continue}m.added&&console.warn(m.value.trim().split(`
|
|
8
|
+
`).map(v=>` + ${v}`).join(`
|
|
9
|
+
`)),m.removed&&console.warn(m.value.trim().split(`
|
|
10
|
+
`).map(v=>` - ${v}`).join(`
|
|
11
|
+
`))}break;case"PASS":console.info(` ${f} - ${t.split(".json").at(0)}: ${i.name} `.green);break;default:console.info(` ${f} - ${t.split(".json").at(0)}: ${i.name} `.bgRed);break}}}break}default:console.error("Unknown command. Did you mean any of these: status, pull, push, test?".red)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rudderstash",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Rudderstack transformation version control, deployment and testing.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rudderstack"
|
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
"rudderstash": "./dist/rudderstash.js"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
-
"build": "esbuild rudderstash.ts --bundle --platform=node --format=esm --outfile=dist/rudderstash.js --banner:js='#!/usr/bin/env node' --packages=external",
|
|
24
|
-
"build:minify": "esbuild rudderstash.ts --bundle --platform=node --format=esm --outfile=dist/rudderstash.js --banner:js='#!/usr/bin/env node' --packages=external --minify",
|
|
23
|
+
"build": "esbuild rudderstash.ts --bundle --platform=node --format=esm --outfile=dist/rudderstash.js --banner:js='#!/usr/bin/env node' --packages=external --external:*.transformation.js",
|
|
24
|
+
"build:minify": "esbuild rudderstash.ts --bundle --platform=node --format=esm --outfile=dist/rudderstash.js --banner:js='#!/usr/bin/env node' --packages=external --minify --external:*.transformation.js",
|
|
25
25
|
"dev": "tsx rudderstash.ts",
|
|
26
|
+
"check": "tsc",
|
|
26
27
|
"debug": "tsx --inspect-brk rudderstash.ts",
|
|
27
28
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
28
29
|
},
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
"typescript": "^5.9.3"
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
41
|
+
"colors": "^1.4.0",
|
|
40
42
|
"diff": "^8.0.2",
|
|
41
43
|
"dotenv": "^17.2.3",
|
|
42
44
|
"yargs": "^18.0.0"
|