@malloydata/malloy-query-builder 0.0.237-dev250225144145

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/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@malloydata/malloy-query-builder",
3
+ "version": "0.0.237-dev250225144145",
4
+ "license": "MIT",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "homepage": "https://github.com/malloydata/malloy#readme",
8
+ "engines": {
9
+ "node": ">=20"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/malloydata/malloy"
14
+ },
15
+ "scripts": {
16
+ "test": "jest --config=../../jest.config.js",
17
+ "clean": "tsc --build --clean",
18
+ "build": "tsc --build && npm run generate-flow",
19
+ "prepublishOnly": "npm run clean && npm run build",
20
+ "generate-flow": "ts-node scripts/gen_flow.ts",
21
+ "generate-docs": "npx typedoc --excludeInternal",
22
+ "serve-docs": "npx http-server -o docs"
23
+ },
24
+ "dependencies": {
25
+ "@malloydata/malloy-filter": "^0.0.237-dev250225144145",
26
+ "@malloydata/malloy-interfaces": "^0.0.237-dev250225144145",
27
+ "@malloydata/malloy-tag": "^0.0.237-dev250225144145"
28
+ },
29
+ "devDependencies": {
30
+ "flow-api-translator": "^0.26.0",
31
+ "http-server": "^14.1.1",
32
+ "typedoc": "^0.27.7"
33
+ }
34
+ }
@@ -0,0 +1,3 @@
1
+ declare module 'flow-api-translator' {
2
+ async function unstable_translateTSDefToFlowDef(file: string): Promise<string>;
3
+ }
@@ -0,0 +1,23 @@
1
+ // eslint-disable-next-line node/no-unpublished-import
2
+ import {unstable_translateTSDefToFlowDef} from 'flow-api-translator';
3
+ import * as fs from 'fs';
4
+
5
+ async function go() {
6
+ const skipFiles = ['expects.d.ts', 'query-ast.spec.d.ts'];
7
+ const files = fs
8
+ .readdirSync('./dist')
9
+ .filter(f => f.endsWith('.d.ts') && !skipFiles.includes(f));
10
+ if (fs.existsSync('./flow')) fs.rmdirSync('./flow', {recursive: true});
11
+ fs.mkdirSync('./flow');
12
+ await Promise.all(
13
+ files.map(async file => {
14
+ // eslint-disable-next-line no-console
15
+ console.log(`Generating flow types for file ${file}`);
16
+ const contents = fs.readFileSync(`./dist/${file}`, 'utf8');
17
+ const flow = await unstable_translateTSDefToFlowDef(contents);
18
+ await fs.promises.writeFile(`./flow/${file}`, flow);
19
+ })
20
+ );
21
+ }
22
+
23
+ go();
package/src/expects.ts ADDED
@@ -0,0 +1,194 @@
1
+ /*
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import * as Malloy from '@malloydata/malloy-interfaces';
9
+ import {ASTQuery} from './query-ast';
10
+
11
+ declare global {
12
+ // eslint-disable-next-line @typescript-eslint/no-namespace
13
+ namespace jest {
14
+ interface Matchers<R> {
15
+ /**
16
+ * expect(q => q.getOrCreateDefaultSegment().addGroupBy('carrier')).toModifyQuery({
17
+ * from: ...,
18
+ * to: ...,
19
+ * malloy: 'run: flights -> { group_by: carrier }'
20
+ * });
21
+ */
22
+ toModifyQuery(exp: {
23
+ model: Malloy.ModelInfo;
24
+ from: Malloy.Query;
25
+ to: Malloy.Query;
26
+ malloy: string;
27
+ }): R;
28
+ toModifyQuery(exp: {
29
+ source: Malloy.SourceInfo;
30
+ from: Malloy.Query;
31
+ to: Malloy.Query;
32
+ malloy: string;
33
+ }): R;
34
+ }
35
+ }
36
+ }
37
+
38
+ expect.extend({
39
+ toModifyQuery(
40
+ f: (q: ASTQuery) => void,
41
+ {
42
+ model,
43
+ source,
44
+ from,
45
+ to,
46
+ malloy,
47
+ }: {
48
+ model?: Malloy.ModelInfo;
49
+ source?: Malloy.SourceInfo;
50
+ from: Malloy.Query;
51
+ to: Malloy.Query;
52
+ malloy: string;
53
+ }
54
+ ) {
55
+ const clone = JSON.parse(JSON.stringify(from));
56
+ const q = model
57
+ ? new ASTQuery({model, query: from})
58
+ : source
59
+ ? new ASTQuery({source, query: from})
60
+ : undefined;
61
+ if (q === undefined) {
62
+ throw new Error('Must specify either model or source');
63
+ }
64
+ f(q);
65
+ const query = q.build();
66
+ const eq = objectsMatch(query, to);
67
+ const diff = this.utils.diff(to, query);
68
+ if (!eq) {
69
+ return {
70
+ pass: false,
71
+ message: () => `Modified query object does not match expected: ${diff}`,
72
+ };
73
+ }
74
+ try {
75
+ ensureOnlyMinimalEdits(from, query, clone);
76
+ } catch (error) {
77
+ return {
78
+ pass: false,
79
+ message: () =>
80
+ `Resulting query object should have minimal edits: ${error.message}`,
81
+ };
82
+ }
83
+ const actualMalloy = q.toMalloy();
84
+ const malloyDiff = this.utils.diff(malloy, actualMalloy);
85
+ if (malloy !== actualMalloy) {
86
+ return {
87
+ pass: false,
88
+ message: () =>
89
+ `Resulting query text does not match expected: ${malloyDiff}`,
90
+ };
91
+ }
92
+ return {
93
+ pass: true,
94
+ message: () => 'Result matched',
95
+ };
96
+ },
97
+ });
98
+
99
+ function ensureOnlyMinimalEdits(
100
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
101
+ a: any,
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ b: any,
104
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
105
+ aClone: any,
106
+ path: (string | number)[] = []
107
+ ): boolean {
108
+ if (
109
+ typeof a === 'string' ||
110
+ typeof a === 'number' ||
111
+ typeof a === 'boolean'
112
+ ) {
113
+ return aClone !== b;
114
+ }
115
+ let different = false;
116
+ if (Array.isArray(a)) {
117
+ different = aClone.length !== b.length;
118
+ for (let i = 0; i < aClone.length || i < b.length; i++) {
119
+ if (a === undefined || b === undefined) {
120
+ different = true;
121
+ } else if (aClone[i] === b[i]) {
122
+ different ||= ensureOnlyMinimalEdits(a[i], b[i], aClone[i], [
123
+ ...path,
124
+ i,
125
+ ]);
126
+ } else {
127
+ different = true;
128
+ const found = aClone.findIndex(f => f === b[i]);
129
+ if (found !== -1) {
130
+ ensureOnlyMinimalEdits(a[found], b[i], aClone[found], [...path, i]);
131
+ }
132
+ }
133
+ }
134
+ } else {
135
+ for (const key in aClone) {
136
+ different ||= ensureOnlyMinimalEdits(a[key], b[key], aClone[key], [
137
+ ...path,
138
+ key,
139
+ ]);
140
+ }
141
+ for (const key in b) {
142
+ if (key in aClone) continue;
143
+ different = true;
144
+ }
145
+ }
146
+ const sameObject = a === b;
147
+ if (different) {
148
+ if (sameObject) {
149
+ throw new Error(`Path /${path.join('/')} was illegally mutated`);
150
+ }
151
+ } else {
152
+ if (!sameObject) {
153
+ throw new Error(`Path /${path.join('/')} was unnecessarily cloned`);
154
+ }
155
+ }
156
+ return different;
157
+ }
158
+
159
+ function objectsMatch(a: unknown, b: unknown): boolean {
160
+ if (
161
+ typeof b === 'string' ||
162
+ typeof b === 'number' ||
163
+ typeof b === 'boolean' ||
164
+ typeof b === 'bigint' ||
165
+ b === undefined ||
166
+ b === null
167
+ ) {
168
+ return b === a;
169
+ } else if (Array.isArray(b)) {
170
+ if (Array.isArray(a)) {
171
+ return a.length === b.length && a.every((v, i) => objectsMatch(v, b[i]));
172
+ }
173
+ return false;
174
+ } else {
175
+ if (
176
+ typeof a === 'string' ||
177
+ typeof a === 'number' ||
178
+ typeof a === 'boolean' ||
179
+ typeof a === 'bigint' ||
180
+ a === undefined ||
181
+ a === null
182
+ ) {
183
+ return false;
184
+ }
185
+ if (Array.isArray(a)) return false;
186
+ const keys = Object.keys(b);
187
+ for (const key of keys) {
188
+ if (!objectsMatch(a[key], b[key])) {
189
+ return false;
190
+ }
191
+ }
192
+ return true;
193
+ }
194
+ }