datajunction 0.0.1-a100

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/.babelrc ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "presets": [
3
+ "@babel/preset-env"
4
+ ],
5
+ "test": [
6
+ "jest"
7
+ ]
8
+ }
package/.eslintignore ADDED
@@ -0,0 +1 @@
1
+ ./dist/*
package/.eslintrc.js ADDED
@@ -0,0 +1,13 @@
1
+ module.exports = {
2
+ env: {
3
+ browser: true,
4
+ es2021: true,
5
+ },
6
+ extends: 'standard',
7
+ overrides: [],
8
+ parserOptions: {
9
+ ecmaVersion: 'latest',
10
+ sourceType: 'module',
11
+ },
12
+ rules: {},
13
+ }
@@ -0,0 +1 @@
1
+ ./dist
package/.prettierrc ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "trailingComma": "es5",
3
+ "tabWidth": 4,
4
+ "semi": false,
5
+ "singleQuote": true
6
+ }
package/Makefile ADDED
@@ -0,0 +1,3 @@
1
+ dev-release:
2
+ yarn version --prerelease --preid dev --no-git-tag-version
3
+ npm publish
@@ -0,0 +1 @@
1
+ module.exports = {presets: ['@babel/preset-env']}
package/index.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./dist')
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "datajunction",
3
+ "version": "0.0.1a100",
4
+ "description": "A Javascript client for interacting with a DataJunction server",
5
+ "module": "src/index.js",
6
+ "scripts": {
7
+ "test": "jest",
8
+ "test-watch": "jest --watch",
9
+ "build": "rm -rf dist/* && webpack && babel src -d dist",
10
+ "lint": "prettier \"src/**/*.{js,jsx}\"",
11
+ "format": "prettier --write \"src/**/*.{js,jsx}\""
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/DataJunction/dj.git"
16
+ },
17
+ "keywords": [
18
+ "datajunction",
19
+ "metrics",
20
+ "metrics-platform",
21
+ "semantic-layer"
22
+ ],
23
+ "author": "DataJunction Authors",
24
+ "license": "MIT",
25
+ "bugs": {
26
+ "url": "https://github.com/DataJunction/dj/issues"
27
+ },
28
+ "homepage": "https://github.com/DataJunction/dj#readme",
29
+ "devDependencies": {
30
+ "@babel/cli": "^7.0.0",
31
+ "@babel/core": "^7.0.0",
32
+ "@babel/plugin-proposal-object-rest-spread": "^7.0.0",
33
+ "@babel/preset-env": "^7.0.0",
34
+ "babel-core": "^7.0.0-bridge.0",
35
+ "babel-jest": "^23.4.2",
36
+ "eslint": "^8.39.0",
37
+ "eslint-config-standard": "^17.0.0",
38
+ "eslint-plugin-import": "^2.27.5",
39
+ "eslint-plugin-n": "^15.7.0",
40
+ "eslint-plugin-promise": "^6.1.1",
41
+ "jest": "^29.5.0",
42
+ "prettier": "^2.8.8",
43
+ "webpack": "^5.81.0",
44
+ "webpack-cli": "^5.0.2"
45
+ },
46
+ "dependencies": {
47
+ "@babel/core": "^7.22.5",
48
+ "docker-names": "^1.2.1"
49
+ }
50
+ }
@@ -0,0 +1,122 @@
1
+ export default class HttpClient {
2
+ constructor(options = {}) {
3
+ this._baseURL = options.baseURL || '';
4
+ this._headers = options.headers || {};
5
+ this._cookie = '';
6
+ }
7
+
8
+ async _fetchJSON(endpoint, options = {}) {
9
+ const res = await fetch(this._baseURL + endpoint, {
10
+ ...options,
11
+ headers: {
12
+ ...this._headers,
13
+ 'Cookie': this._cookie,
14
+ },
15
+ credentials: 'include',
16
+ });
17
+
18
+ const setCookieHeader = res.headers.get('Set-Cookie');
19
+ if (setCookieHeader) {
20
+ this._cookie = setCookieHeader;
21
+ this._headers['Cookie'] = this._cookie;
22
+ }
23
+
24
+ if (!res.ok) {
25
+ const errorText = await res.text();
26
+ throw new Error(`Request failed: ${res.status} ${errorText}`);
27
+ }
28
+
29
+ if (options.parseResponse !== false && res.status !== 204) {
30
+ return res.json();
31
+ }
32
+
33
+ return undefined;
34
+ }
35
+
36
+ async login(username, password) {
37
+ const body = new URLSearchParams({
38
+ grant_type: 'password',
39
+ username: username,
40
+ password: password,
41
+ });
42
+
43
+ const response = await fetch(this._baseURL + '/basic/login', {
44
+ method: 'POST',
45
+ headers: {
46
+ 'Content-Type': 'application/x-www-form-urlencoded',
47
+ ...this._headers,
48
+ },
49
+ body: body.toString(),
50
+ credentials: 'include',
51
+ });
52
+
53
+ if (!response.ok) {
54
+ const errorText = await response.text();
55
+ throw new Error(`Login failed: ${response.status} ${errorText}`);
56
+ }
57
+
58
+ const setCookieHeader = response.headers.get('Set-Cookie');
59
+ if (setCookieHeader) {
60
+ this._cookie = setCookieHeader;
61
+ this._headers['Cookie'] = this._cookie;
62
+ }
63
+ }
64
+ setHeader(key, value) {
65
+ this._headers[key] = value
66
+ return this
67
+ }
68
+
69
+ getHeader(key) {
70
+ return this._headers[key]
71
+ }
72
+
73
+ setBasicAuth(username, password) {
74
+ this._headers.Authorization = `Basic ${btoa(`${username}:${password}`)}`
75
+ return this
76
+ }
77
+
78
+ setBearerAuth(token) {
79
+ this._headers.Authorization = `Bearer ${token}`
80
+ return this
81
+ }
82
+
83
+ async get(endpoint, options = {}) {
84
+ return this._fetchJSON(endpoint, {
85
+ ...options,
86
+ method: 'GET',
87
+ })
88
+ }
89
+
90
+ async post(endpoint, body, options = {}) {
91
+ return this._fetchJSON(endpoint, {
92
+ ...options,
93
+ body: body ? JSON.stringify(body) : undefined,
94
+ method: 'POST',
95
+ })
96
+ }
97
+
98
+ async put(endpoint, body, options = {}) {
99
+ return this._fetchJSON(endpoint, {
100
+ ...options,
101
+ body: body ? JSON.stringify(body) : undefined,
102
+ method: 'PUT',
103
+ })
104
+ }
105
+
106
+ async patch(endpoint, operations, options = {}) {
107
+ return this._fetchJSON(endpoint, {
108
+ parseResponse: false,
109
+ ...options,
110
+ body: JSON.stringify(operations),
111
+ method: 'PATCH',
112
+ })
113
+ }
114
+
115
+ async delete(endpoint, options = {}) {
116
+ return this._fetchJSON(endpoint, {
117
+ parseResponse: false,
118
+ ...options,
119
+ method: 'DELETE',
120
+ })
121
+ }
122
+ }
package/src/index.js ADDED
@@ -0,0 +1,314 @@
1
+ import HttpClient from './httpclient.js'
2
+
3
+ export class DJClient extends HttpClient {
4
+ constructor(
5
+ baseURL,
6
+ namespace,
7
+ engineName = null,
8
+ engineVersion = null,
9
+ httpAgent = null,
10
+ ) {
11
+ super(
12
+ {
13
+ baseURL,
14
+ },
15
+ httpAgent
16
+ )
17
+ this.namespace = namespace
18
+ this.engineName = engineName
19
+ this.engineVersion = engineVersion
20
+ }
21
+
22
+ get healthcheck() {
23
+ return {
24
+ get: () => this.get('/health/'),
25
+ }
26
+ }
27
+
28
+ get catalogs() {
29
+ return {
30
+ list: () => this.get('/catalogs/'),
31
+ get: (catalog) => this.get(`/catalogs/${catalog}/`),
32
+ create: (catalog) =>
33
+ this.setHeader('Content-Type', 'application/json').post(
34
+ `/catalogs/`,
35
+ catalog
36
+ ),
37
+ addEngine: (catalog, engineName, engineVersion) =>
38
+ this.setHeader('Content-Type', 'application/json').post(
39
+ `/catalogs/${catalog}/engines/`,
40
+ [
41
+ {
42
+ name: engineName,
43
+ version: engineVersion,
44
+ },
45
+ ]
46
+ ),
47
+ }
48
+ }
49
+
50
+ get engines() {
51
+ return {
52
+ list: () => this.get('/engines/'),
53
+ get: (engineName, engineVersion) =>
54
+ this.get(`/engines/${engineName}/${engineVersion}/`),
55
+ create: (engine) => this.post('/engines/', engine),
56
+ }
57
+ }
58
+
59
+ get addEngineToCatalog() {
60
+ return {
61
+ set: (catalogName, engine) =>
62
+ this.setHeader('Content-Type', 'application/json').post(
63
+ `/catalogs/${catalogName}/engines/`,
64
+ [engine]
65
+ ),
66
+ }
67
+ }
68
+
69
+ get namespaces() {
70
+ return {
71
+ list: () => this.get('/namespaces/'),
72
+ nodes: (namespace) => this.get(`/namespaces/${namespace}/`),
73
+ create: (namespace) =>
74
+ this.setHeader('Content-Type', 'application/json').post(
75
+ `/namespaces/${namespace}/`
76
+ ),
77
+ }
78
+ }
79
+
80
+ get commonDimensions() {
81
+ return {
82
+ list: (metrics) => {
83
+ const metricsQuery =
84
+ '?' + metrics.map((m) => `metric=${m}`).join('&')
85
+ return this.get('/metrics/common/dimensions/' + metricsQuery)
86
+ },
87
+ }
88
+ }
89
+
90
+ get nodes() {
91
+ return {
92
+ get: (nodeName) => this.get(`/nodes/${nodeName}/`),
93
+ validate: (nodeDetails) =>
94
+ this.setHeader('Content-Type', 'application/json').post(
95
+ '/nodes/validate/',
96
+ nodeDetails
97
+ ),
98
+ update: (nodeName, nodeDetails) =>
99
+ this.setHeader('Content-Type', 'application/json').patch(
100
+ `/nodes/${nodeName}/`,
101
+ nodeDetails
102
+ ),
103
+ revisions: (nodeName) => this.get(`/nodes/${nodeName}/revisions/`),
104
+ downstream: (nodeName) =>
105
+ this.get(`/nodes/${nodeName}/downstream/`),
106
+ upstream: (nodeName) => this.get(`/nodes/${nodeName}/upstream/`),
107
+ publish: (nodeName) => this.patch(`/nodes/${nodeName}/`, {'mode': 'published'})
108
+ }
109
+ }
110
+
111
+ get sources() {
112
+ return {
113
+ create: (sourceDetails) =>
114
+ this.setHeader('Content-Type', 'application/json').post(
115
+ '/nodes/source/',
116
+ sourceDetails
117
+ ),
118
+ list: () => this.get(`/namespaces/${this.namespace}/?type_=source`),
119
+ }
120
+ }
121
+
122
+ get transforms() {
123
+ return {
124
+ create: (transformDetails) =>
125
+ this.setHeader('Content-Type', 'application/json').post(
126
+ '/nodes/transform/',
127
+ transformDetails
128
+ ),
129
+ list: () =>
130
+ this.get(`/namespaces/${this.namespace}/?type_=transform`),
131
+ }
132
+ }
133
+
134
+ get dimensions() {
135
+ return {
136
+ create: (dimensionDetails) =>
137
+ this.setHeader('Content-Type', 'application/json').post(
138
+ '/nodes/dimension/',
139
+ dimensionDetails
140
+ ),
141
+ list: () =>
142
+ this.get(`/namespaces/${this.namespace}/?type_=dimension`),
143
+ link: (nodeName, nodeColumn, dimension, dimensionColumn) =>
144
+ this.post(
145
+ `/nodes/${nodeName}/columns/${nodeColumn}/?dimension=${dimension}&dimension_column=${dimensionColumn}`
146
+ ),
147
+ }
148
+ }
149
+
150
+ get metrics() {
151
+ return {
152
+ get: (metricName) => this.get(`/metrics/${metricName}/`),
153
+ create: (metricDetails) =>
154
+ this.setHeader('Content-Type', 'application/json').post(
155
+ '/nodes/metric/',
156
+ metricDetails
157
+ ),
158
+ list: () => this.get(`/namespaces/${this.namespace}/?type_=metric`),
159
+ all: () => this.get(`/metrics/`),
160
+ }
161
+ }
162
+
163
+ get cubes() {
164
+ return {
165
+ get: (cubeName) => this.get(`/cubes/${cubeName}/`),
166
+ create: (cubeDetails) =>
167
+ this.setHeader('Content-Type', 'application/json').post(
168
+ '/nodes/cube/',
169
+ cubeDetails
170
+ ),
171
+ }
172
+ }
173
+
174
+ get tags() {
175
+ return {
176
+ list: () => this.get('/tags/'),
177
+ get: (tagName) => this.get(`/tags/${tagName}/`),
178
+ create: (tagData) =>
179
+ this.setHeader('Content-Type', 'application/json').post(
180
+ '/tags/',
181
+ tagData
182
+ ),
183
+ update: (tagName, tagData) =>
184
+ this.setHeader('Content-Type', 'application/json').patch(
185
+ `/tags/${tagName}/`,
186
+ tagData
187
+ ),
188
+ set: (nodeName, tagName) =>
189
+ this.post(`/nodes/${nodeName}/tag/?tag_name=${tagName}`),
190
+ listNodes: (tagName) => this.get(`/tags/${tagName}/nodes/`),
191
+ }
192
+ }
193
+
194
+ get attributes() {
195
+ return {
196
+ list: () => this.get('/attributes/'),
197
+ create: (attributeData) =>
198
+ this.setHeader('Content-Type', 'application/json').post(
199
+ '/attributes/',
200
+ attributeData
201
+ ),
202
+ }
203
+ }
204
+
205
+ get materializationConfigs() {
206
+ return {
207
+ update: (nodeName, materializationDetails) =>
208
+ this.setHeader('Content-Type', 'application/json').post(
209
+ `/nodes/${nodeName}/materialization/`,
210
+ materializationDetails
211
+ ),
212
+ }
213
+ }
214
+
215
+ get columnAttributes() {
216
+ return {
217
+ set: (nodeName, columnAttribute) =>
218
+ this.setHeader('Content-Type', 'application/json').post(
219
+ `/nodes/${nodeName}/attributes/`,
220
+ [columnAttribute]
221
+ ),
222
+ }
223
+ }
224
+
225
+ get availabilityState() {
226
+ return {
227
+ set: (nodeName, availabilityState) =>
228
+ this.setHeader('Content-Type', 'application/json').post(
229
+ `/data/${nodeName}/availability/`,
230
+ availabilityState
231
+ ),
232
+ }
233
+ }
234
+
235
+ get sql() {
236
+ return {
237
+ get: (
238
+ metrics,
239
+ dimensions,
240
+ filters,
241
+ engineName = null,
242
+ engineVersion = null
243
+ ) => {
244
+ const metricsQuery =
245
+ '?' + metrics.map((m) => `metrics=${m}`).join('&')
246
+ const dimensionsQuery = dimensions
247
+ .map((d) => `dimensions=${d}`)
248
+ .join('&')
249
+ const filtersQuery = filters
250
+ .map((f) => `filters=${f}`)
251
+ .join('&')
252
+ const engineNameP = engineName ? `&engine=${engineName}` : ''
253
+ const engineVersionP = engineVersion
254
+ ? `&engine_version=${engineVersion}`
255
+ : ''
256
+ return this.get(
257
+ `/sql/${metricsQuery}&${dimensionsQuery}${filtersQuery}${engineNameP}${engineVersionP}`
258
+ )
259
+ },
260
+ }
261
+ }
262
+
263
+ get data() {
264
+ return {
265
+ get: (
266
+ metrics,
267
+ dimensions,
268
+ filters,
269
+ async_ = false,
270
+ engineName = null,
271
+ engineVersion = null
272
+ ) => {
273
+ const metricsQuery =
274
+ '?' + metrics.map((m) => `metrics=${m}`).join('&')
275
+ const dimensionsQuery = dimensions
276
+ .map((d) => `dimensions=${d}`)
277
+ .join('&')
278
+ const filtersQuery = filters
279
+ .map((f) => `filters=${f}`)
280
+ .join('&')
281
+ const asyncP = async_ ? `&async_=${async_}` : ''
282
+ const engineNameP = engineName ? `&engine=${engineName}` : ''
283
+ const engineVersionP = engineVersion
284
+ ? `&engine_version=${engineVersion}`
285
+ : ''
286
+ const data = this.get(
287
+ `/data/${metricsQuery}&${dimensionsQuery}${filtersQuery}${asyncP}${engineNameP}${engineVersionP}`
288
+ ).then((data) => {
289
+ return {
290
+ columns: data.results[0].columns,
291
+ data: data.results[0].rows,
292
+ }
293
+ })
294
+ return data
295
+ },
296
+ }
297
+ }
298
+
299
+ get register() {
300
+ return {
301
+ table: (catalog, schema, table) =>
302
+ this.setHeader('Content-Type', 'application/json').post(
303
+ `/register/table/${catalog}/${schema}/${table}`
304
+ ),
305
+ view: (catalog, schema, view, query, replace = false) => {
306
+ const replaceQuery = replace ? `?replace=${replace}` : '';
307
+ return this.setHeader('Content-Type', 'application/json').post(
308
+ `/register/view/${catalog}/${schema}/${view}${replaceQuery}`,
309
+ { query }
310
+ );
311
+ }
312
+ }
313
+ }
314
+ }
@@ -0,0 +1,80 @@
1
+ const { DJClient } = require('./index')
2
+ var dockerNames = require('docker-names')
3
+
4
+ test('should return something', async () => {
5
+ const dj = new DJClient('http://localhost:8000', 'integration.tests', 'dj', 'dj');
6
+ await dj.login("dj", "dj")
7
+
8
+ await dj.catalogs.create({ name: 'tpch' });
9
+ await dj.engines.create({ name: 'trino', version: '451' });
10
+ await dj.catalogs.addEngine('tpch', 'trino', '451');
11
+
12
+ await dj.namespaces.create('integration.tests');
13
+ await dj.namespaces.create('integration.tests.trino');
14
+
15
+ const source = await dj.sources.create({
16
+ name: 'integration.tests.source1',
17
+ catalog: 'unknown',
18
+ schema_: 'db',
19
+ table: 'tbl',
20
+ display_name: 'Test Source with Columns',
21
+ description: 'A test source node with columns',
22
+ columns: [
23
+ { name: 'id', type: 'int' },
24
+ { name: 'name', type: 'string' },
25
+ { name: 'price', type: 'double' },
26
+ { name: 'created_at', type: 'timestamp' },
27
+ ],
28
+ primary_key: ['id'],
29
+ mode: 'published',
30
+ update_if_exists: true,
31
+ });
32
+
33
+ await dj.register.table('tpch', 'sf1', 'orders');
34
+
35
+ const transform = await dj.transforms.create({
36
+ name: 'integration.tests.trino.transform1',
37
+ display_name: 'Filter to last 1000 records',
38
+ description: 'The last 1000 purchases',
39
+ mode: 'published',
40
+ query: 'select custkey, totalprice, orderdate from source.tpch.sf1.orders order by orderdate desc limit 1000',
41
+ update_if_exists: true,
42
+ });
43
+
44
+ const dimension = await dj.dimensions.create({
45
+ name: 'integration.tests.trino.dimension1',
46
+ display_name: 'Customer keys',
47
+ description: 'All custkey values in the source table',
48
+ mode: 'published',
49
+ primary_key: ['id'],
50
+ tags: [],
51
+ query: "select custkey as id, 'attribute' as foo from source.tpch.sf1.orders",
52
+ update_if_exists: true,
53
+ });
54
+
55
+ await dj.dimensions.link(
56
+ 'integration.tests.trino.transform1',
57
+ 'custkey',
58
+ 'integration.tests.trino.dimension1',
59
+ 'id',
60
+ );
61
+
62
+ const metric = await dj.metrics.create({
63
+ name: 'integration.tests.trino.metric1',
64
+ display_name: 'Total of last 1000 purchases',
65
+ description: 'This is the total amount from the last 1000 purchases',
66
+ mode: 'published',
67
+ query: 'select sum(totalprice) from integration.tests.trino.transform1',
68
+ update_if_exists: true,
69
+ });
70
+
71
+ await dj.commonDimensions.list(['integration.tests.trino.metric1']);
72
+
73
+ const query = await dj.sql.get(
74
+ ['integration.tests.trino.metric1'],
75
+ ['integration.tests.trino.dimension1.id'],
76
+ []
77
+ );
78
+
79
+ expect(query.sql).toContain('SELECT');
80
+ }, 60000)
@@ -0,0 +1,15 @@
1
+ const path = require('path')
2
+
3
+ module.exports = {
4
+ entry: './src/index.js',
5
+ mode: 'production',
6
+ output: {
7
+ path: path.resolve(__dirname, 'dist'),
8
+ filename: 'datajunction.js',
9
+ globalObject: 'this',
10
+ library: {
11
+ name: 'datajunction',
12
+ type: 'umd',
13
+ },
14
+ },
15
+ }