@tinybirdco/sdk 0.0.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.
Files changed (258) hide show
  1. package/README.md +518 -0
  2. package/bin/tinybird.js +7 -0
  3. package/dist/api/branches.d.ts +98 -0
  4. package/dist/api/branches.d.ts.map +1 -0
  5. package/dist/api/branches.js +203 -0
  6. package/dist/api/branches.js.map +1 -0
  7. package/dist/api/branches.test.d.ts +2 -0
  8. package/dist/api/branches.test.d.ts.map +1 -0
  9. package/dist/api/branches.test.js +286 -0
  10. package/dist/api/branches.test.js.map +1 -0
  11. package/dist/api/build.d.ts +130 -0
  12. package/dist/api/build.d.ts.map +1 -0
  13. package/dist/api/build.js +143 -0
  14. package/dist/api/build.js.map +1 -0
  15. package/dist/api/build.test.d.ts +2 -0
  16. package/dist/api/build.test.d.ts.map +1 -0
  17. package/dist/api/build.test.js +138 -0
  18. package/dist/api/build.test.js.map +1 -0
  19. package/dist/api/deploy.d.ts +39 -0
  20. package/dist/api/deploy.d.ts.map +1 -0
  21. package/dist/api/deploy.js +135 -0
  22. package/dist/api/deploy.js.map +1 -0
  23. package/dist/api/deploy.test.d.ts +2 -0
  24. package/dist/api/deploy.test.d.ts.map +1 -0
  25. package/dist/api/deploy.test.js +118 -0
  26. package/dist/api/deploy.test.js.map +1 -0
  27. package/dist/api/workspaces.d.ts +46 -0
  28. package/dist/api/workspaces.d.ts.map +1 -0
  29. package/dist/api/workspaces.js +39 -0
  30. package/dist/api/workspaces.js.map +1 -0
  31. package/dist/api/workspaces.test.d.ts +2 -0
  32. package/dist/api/workspaces.test.d.ts.map +1 -0
  33. package/dist/api/workspaces.test.js +65 -0
  34. package/dist/api/workspaces.test.js.map +1 -0
  35. package/dist/cli/auth.d.ts +86 -0
  36. package/dist/cli/auth.d.ts.map +1 -0
  37. package/dist/cli/auth.js +284 -0
  38. package/dist/cli/auth.js.map +1 -0
  39. package/dist/cli/branch-store.d.ts +53 -0
  40. package/dist/cli/branch-store.d.ts.map +1 -0
  41. package/dist/cli/branch-store.js +91 -0
  42. package/dist/cli/branch-store.js.map +1 -0
  43. package/dist/cli/branch-store.test.d.ts +2 -0
  44. package/dist/cli/branch-store.test.d.ts.map +1 -0
  45. package/dist/cli/branch-store.test.js +115 -0
  46. package/dist/cli/branch-store.test.js.map +1 -0
  47. package/dist/cli/commands/branch.d.ts +82 -0
  48. package/dist/cli/commands/branch.d.ts.map +1 -0
  49. package/dist/cli/commands/branch.js +215 -0
  50. package/dist/cli/commands/branch.js.map +1 -0
  51. package/dist/cli/commands/build.d.ts +43 -0
  52. package/dist/cli/commands/build.d.ts.map +1 -0
  53. package/dist/cli/commands/build.js +138 -0
  54. package/dist/cli/commands/build.js.map +1 -0
  55. package/dist/cli/commands/dev.d.ts +78 -0
  56. package/dist/cli/commands/dev.d.ts.map +1 -0
  57. package/dist/cli/commands/dev.js +226 -0
  58. package/dist/cli/commands/dev.js.map +1 -0
  59. package/dist/cli/commands/init.d.ts +45 -0
  60. package/dist/cli/commands/init.d.ts.map +1 -0
  61. package/dist/cli/commands/init.js +277 -0
  62. package/dist/cli/commands/init.js.map +1 -0
  63. package/dist/cli/commands/init.test.d.ts +2 -0
  64. package/dist/cli/commands/init.test.d.ts.map +1 -0
  65. package/dist/cli/commands/init.test.js +158 -0
  66. package/dist/cli/commands/init.test.js.map +1 -0
  67. package/dist/cli/commands/login.d.ts +37 -0
  68. package/dist/cli/commands/login.d.ts.map +1 -0
  69. package/dist/cli/commands/login.js +64 -0
  70. package/dist/cli/commands/login.js.map +1 -0
  71. package/dist/cli/config.d.ts +114 -0
  72. package/dist/cli/config.d.ts.map +1 -0
  73. package/dist/cli/config.js +258 -0
  74. package/dist/cli/config.js.map +1 -0
  75. package/dist/cli/config.test.d.ts +2 -0
  76. package/dist/cli/config.test.d.ts.map +1 -0
  77. package/dist/cli/config.test.js +243 -0
  78. package/dist/cli/config.test.js.map +1 -0
  79. package/dist/cli/env.d.ts +29 -0
  80. package/dist/cli/env.d.ts.map +1 -0
  81. package/dist/cli/env.js +66 -0
  82. package/dist/cli/env.js.map +1 -0
  83. package/dist/cli/git.d.ts +29 -0
  84. package/dist/cli/git.d.ts.map +1 -0
  85. package/dist/cli/git.js +114 -0
  86. package/dist/cli/git.js.map +1 -0
  87. package/dist/cli/git.test.d.ts +2 -0
  88. package/dist/cli/git.test.d.ts.map +1 -0
  89. package/dist/cli/git.test.js +125 -0
  90. package/dist/cli/git.test.js.map +1 -0
  91. package/dist/cli/index.d.ts +7 -0
  92. package/dist/cli/index.d.ts.map +1 -0
  93. package/dist/cli/index.js +337 -0
  94. package/dist/cli/index.js.map +1 -0
  95. package/dist/cli/utils/schema-validation.d.ts +95 -0
  96. package/dist/cli/utils/schema-validation.d.ts.map +1 -0
  97. package/dist/cli/utils/schema-validation.js +175 -0
  98. package/dist/cli/utils/schema-validation.js.map +1 -0
  99. package/dist/cli/utils/schema-validation.test.d.ts +5 -0
  100. package/dist/cli/utils/schema-validation.test.d.ts.map +1 -0
  101. package/dist/cli/utils/schema-validation.test.js +173 -0
  102. package/dist/cli/utils/schema-validation.test.js.map +1 -0
  103. package/dist/client/base.d.ts +116 -0
  104. package/dist/client/base.d.ts.map +1 -0
  105. package/dist/client/base.js +328 -0
  106. package/dist/client/base.js.map +1 -0
  107. package/dist/client/types.d.ts +137 -0
  108. package/dist/client/types.d.ts.map +1 -0
  109. package/dist/client/types.js +43 -0
  110. package/dist/client/types.js.map +1 -0
  111. package/dist/generator/client.d.ts +44 -0
  112. package/dist/generator/client.d.ts.map +1 -0
  113. package/dist/generator/client.js +144 -0
  114. package/dist/generator/client.js.map +1 -0
  115. package/dist/generator/datasource.d.ts +57 -0
  116. package/dist/generator/datasource.d.ts.map +1 -0
  117. package/dist/generator/datasource.js +169 -0
  118. package/dist/generator/datasource.js.map +1 -0
  119. package/dist/generator/datasource.test.d.ts +2 -0
  120. package/dist/generator/datasource.test.d.ts.map +1 -0
  121. package/dist/generator/datasource.test.js +254 -0
  122. package/dist/generator/datasource.test.js.map +1 -0
  123. package/dist/generator/index.d.ts +131 -0
  124. package/dist/generator/index.d.ts.map +1 -0
  125. package/dist/generator/index.js +121 -0
  126. package/dist/generator/index.js.map +1 -0
  127. package/dist/generator/index.test.d.ts +2 -0
  128. package/dist/generator/index.test.d.ts.map +1 -0
  129. package/dist/generator/index.test.js +175 -0
  130. package/dist/generator/index.test.js.map +1 -0
  131. package/dist/generator/loader.d.ts +156 -0
  132. package/dist/generator/loader.d.ts.map +1 -0
  133. package/dist/generator/loader.js +295 -0
  134. package/dist/generator/loader.js.map +1 -0
  135. package/dist/generator/pipe.d.ts +72 -0
  136. package/dist/generator/pipe.d.ts.map +1 -0
  137. package/dist/generator/pipe.js +174 -0
  138. package/dist/generator/pipe.js.map +1 -0
  139. package/dist/generator/pipe.test.d.ts +2 -0
  140. package/dist/generator/pipe.test.d.ts.map +1 -0
  141. package/dist/generator/pipe.test.js +393 -0
  142. package/dist/generator/pipe.test.js.map +1 -0
  143. package/dist/index.d.ts +74 -0
  144. package/dist/index.d.ts.map +1 -0
  145. package/dist/index.js +73 -0
  146. package/dist/index.js.map +1 -0
  147. package/dist/infer/index.d.ts +202 -0
  148. package/dist/infer/index.d.ts.map +1 -0
  149. package/dist/infer/index.js +5 -0
  150. package/dist/infer/index.js.map +1 -0
  151. package/dist/schema/datasource.d.ts +135 -0
  152. package/dist/schema/datasource.d.ts.map +1 -0
  153. package/dist/schema/datasource.js +105 -0
  154. package/dist/schema/datasource.js.map +1 -0
  155. package/dist/schema/datasource.test.d.ts +2 -0
  156. package/dist/schema/datasource.test.d.ts.map +1 -0
  157. package/dist/schema/datasource.test.js +142 -0
  158. package/dist/schema/datasource.test.js.map +1 -0
  159. package/dist/schema/engines.d.ts +157 -0
  160. package/dist/schema/engines.d.ts.map +1 -0
  161. package/dist/schema/engines.js +155 -0
  162. package/dist/schema/engines.js.map +1 -0
  163. package/dist/schema/engines.test.d.ts +2 -0
  164. package/dist/schema/engines.test.d.ts.map +1 -0
  165. package/dist/schema/engines.test.js +221 -0
  166. package/dist/schema/engines.test.js.map +1 -0
  167. package/dist/schema/params.d.ts +106 -0
  168. package/dist/schema/params.d.ts.map +1 -0
  169. package/dist/schema/params.js +138 -0
  170. package/dist/schema/params.js.map +1 -0
  171. package/dist/schema/params.test.d.ts +2 -0
  172. package/dist/schema/params.test.d.ts.map +1 -0
  173. package/dist/schema/params.test.js +175 -0
  174. package/dist/schema/params.test.js.map +1 -0
  175. package/dist/schema/pipe.d.ts +436 -0
  176. package/dist/schema/pipe.d.ts.map +1 -0
  177. package/dist/schema/pipe.js +484 -0
  178. package/dist/schema/pipe.js.map +1 -0
  179. package/dist/schema/pipe.test.d.ts +2 -0
  180. package/dist/schema/pipe.test.d.ts.map +1 -0
  181. package/dist/schema/pipe.test.js +488 -0
  182. package/dist/schema/pipe.test.js.map +1 -0
  183. package/dist/schema/project.d.ts +202 -0
  184. package/dist/schema/project.d.ts.map +1 -0
  185. package/dist/schema/project.js +188 -0
  186. package/dist/schema/project.js.map +1 -0
  187. package/dist/schema/project.test.d.ts +2 -0
  188. package/dist/schema/project.test.d.ts.map +1 -0
  189. package/dist/schema/project.test.js +180 -0
  190. package/dist/schema/project.test.js.map +1 -0
  191. package/dist/schema/types.d.ts +140 -0
  192. package/dist/schema/types.d.ts.map +1 -0
  193. package/dist/schema/types.js +174 -0
  194. package/dist/schema/types.js.map +1 -0
  195. package/dist/schema/types.test.d.ts +2 -0
  196. package/dist/schema/types.test.d.ts.map +1 -0
  197. package/dist/schema/types.test.js +176 -0
  198. package/dist/schema/types.test.js.map +1 -0
  199. package/dist/test/handlers.d.ts +58 -0
  200. package/dist/test/handlers.d.ts.map +1 -0
  201. package/dist/test/handlers.js +62 -0
  202. package/dist/test/handlers.js.map +1 -0
  203. package/dist/test/setup.d.ts +5 -0
  204. package/dist/test/setup.d.ts.map +1 -0
  205. package/dist/test/setup.js +11 -0
  206. package/dist/test/setup.js.map +1 -0
  207. package/package.json +57 -0
  208. package/src/api/branches.test.ts +377 -0
  209. package/src/api/branches.ts +334 -0
  210. package/src/api/build.test.ts +216 -0
  211. package/src/api/build.ts +266 -0
  212. package/src/api/deploy.test.ts +193 -0
  213. package/src/api/deploy.ts +163 -0
  214. package/src/api/workspaces.test.ts +81 -0
  215. package/src/api/workspaces.ts +77 -0
  216. package/src/cli/auth.ts +358 -0
  217. package/src/cli/branch-store.test.ts +139 -0
  218. package/src/cli/branch-store.ts +137 -0
  219. package/src/cli/commands/branch.ts +306 -0
  220. package/src/cli/commands/build.ts +183 -0
  221. package/src/cli/commands/dev.ts +334 -0
  222. package/src/cli/commands/init.test.ts +249 -0
  223. package/src/cli/commands/init.ts +323 -0
  224. package/src/cli/commands/login.ts +98 -0
  225. package/src/cli/config.test.ts +359 -0
  226. package/src/cli/config.ts +335 -0
  227. package/src/cli/env.ts +86 -0
  228. package/src/cli/git.test.ts +147 -0
  229. package/src/cli/git.ts +125 -0
  230. package/src/cli/index.ts +382 -0
  231. package/src/cli/utils/schema-validation.test.ts +222 -0
  232. package/src/cli/utils/schema-validation.ts +272 -0
  233. package/src/client/base.ts +414 -0
  234. package/src/client/types.ts +165 -0
  235. package/src/generator/client.ts +194 -0
  236. package/src/generator/datasource.test.ts +297 -0
  237. package/src/generator/datasource.ts +217 -0
  238. package/src/generator/index.test.ts +209 -0
  239. package/src/generator/index.ts +203 -0
  240. package/src/generator/loader.ts +406 -0
  241. package/src/generator/pipe.test.ts +441 -0
  242. package/src/generator/pipe.ts +220 -0
  243. package/src/index.ts +191 -0
  244. package/src/infer/index.ts +247 -0
  245. package/src/schema/datasource.test.ts +187 -0
  246. package/src/schema/datasource.ts +195 -0
  247. package/src/schema/engines.test.ts +247 -0
  248. package/src/schema/engines.ts +271 -0
  249. package/src/schema/params.test.ts +208 -0
  250. package/src/schema/params.ts +249 -0
  251. package/src/schema/pipe.test.ts +588 -0
  252. package/src/schema/pipe.ts +832 -0
  253. package/src/schema/project.test.ts +236 -0
  254. package/src/schema/project.ts +394 -0
  255. package/src/schema/types.test.ts +212 -0
  256. package/src/schema/types.ts +366 -0
  257. package/src/test/handlers.ts +79 -0
  258. package/src/test/setup.ts +13 -0
@@ -0,0 +1,441 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generatePipe, generateAllPipes } from './pipe.js';
3
+ import { definePipe, defineMaterializedView, node } from '../schema/pipe.js';
4
+ import { defineDatasource } from '../schema/datasource.js';
5
+ import { t } from '../schema/types.js';
6
+ import { p } from '../schema/params.js';
7
+ import { engine } from '../schema/engines.js';
8
+
9
+ // Helper to create a simple output schema for tests
10
+ const simpleOutput = { result: t.int32() };
11
+
12
+ describe('Pipe Generator', () => {
13
+ describe('generatePipe', () => {
14
+ it('generates basic pipe with node', () => {
15
+ const pipe = definePipe('test_pipe', {
16
+ nodes: [
17
+ node({
18
+ name: 'endpoint',
19
+ sql: 'SELECT * FROM table',
20
+ }),
21
+ ],
22
+ output: simpleOutput,
23
+ endpoint: true,
24
+ });
25
+
26
+ const result = generatePipe(pipe);
27
+ expect(result.name).toBe('test_pipe');
28
+ expect(result.content).toContain('NODE endpoint');
29
+ expect(result.content).toContain('SQL >');
30
+ expect(result.content).toContain('SELECT * FROM table');
31
+ });
32
+
33
+ it('includes description when provided', () => {
34
+ const pipe = definePipe('test_pipe', {
35
+ description: 'Test pipe description',
36
+ nodes: [node({ name: 'endpoint', sql: 'SELECT 1' })],
37
+ output: simpleOutput,
38
+ endpoint: true,
39
+ });
40
+
41
+ const result = generatePipe(pipe);
42
+ expect(result.content).toContain('DESCRIPTION >');
43
+ expect(result.content).toContain('Test pipe description');
44
+ });
45
+
46
+ it('includes TYPE endpoint when endpoint is true', () => {
47
+ const pipe = definePipe('test_pipe', {
48
+ nodes: [node({ name: 'endpoint', sql: 'SELECT 1' })],
49
+ output: simpleOutput,
50
+ endpoint: true,
51
+ });
52
+
53
+ const result = generatePipe(pipe);
54
+ expect(result.content).toContain('TYPE endpoint');
55
+ });
56
+
57
+ it('does not include TYPE endpoint when endpoint is false', () => {
58
+ const pipe = definePipe('test_pipe', {
59
+ nodes: [node({ name: 'endpoint', sql: 'SELECT 1' })],
60
+ output: simpleOutput,
61
+ endpoint: false,
62
+ });
63
+
64
+ const result = generatePipe(pipe);
65
+ expect(result.content).not.toContain('TYPE endpoint');
66
+ });
67
+ });
68
+
69
+ describe('Dynamic SQL detection', () => {
70
+ it('adds % on its own line for SQL with template parameters', () => {
71
+ const pipe = definePipe('test_pipe', {
72
+ nodes: [
73
+ node({
74
+ name: 'endpoint',
75
+ sql: 'SELECT * FROM table WHERE id = {{Int32(id)}}',
76
+ }),
77
+ ],
78
+ output: simpleOutput,
79
+ endpoint: true,
80
+ });
81
+
82
+ const result = generatePipe(pipe);
83
+ expect(result.content).toContain('SQL >\n %\n SELECT');
84
+ });
85
+
86
+ it('adds % for SQL with DateTime parameter', () => {
87
+ const pipe = definePipe('test_pipe', {
88
+ nodes: [
89
+ node({
90
+ name: 'endpoint',
91
+ sql: 'SELECT * FROM table WHERE timestamp >= {{DateTime(start_date)}}',
92
+ }),
93
+ ],
94
+ output: simpleOutput,
95
+ endpoint: true,
96
+ });
97
+
98
+ const result = generatePipe(pipe);
99
+ expect(result.content).toContain(' %\n');
100
+ });
101
+
102
+ it('does not add % for SQL without template parameters', () => {
103
+ const pipe = definePipe('test_pipe', {
104
+ nodes: [
105
+ node({
106
+ name: 'endpoint',
107
+ sql: 'SELECT * FROM table',
108
+ }),
109
+ ],
110
+ output: simpleOutput,
111
+ endpoint: true,
112
+ });
113
+
114
+ const result = generatePipe(pipe);
115
+ expect(result.content).not.toContain('%');
116
+ });
117
+
118
+ it('does not add % for SQL with curly braces that are not parameters', () => {
119
+ const pipe = definePipe('test_pipe', {
120
+ nodes: [
121
+ node({
122
+ name: 'endpoint',
123
+ sql: "SELECT JSONExtract(data, 'field', 'String') FROM table",
124
+ }),
125
+ ],
126
+ output: simpleOutput,
127
+ endpoint: true,
128
+ });
129
+
130
+ const result = generatePipe(pipe);
131
+ expect(result.content).not.toContain('%');
132
+ });
133
+ });
134
+
135
+ describe('Multiple nodes', () => {
136
+ it('generates all nodes with separation', () => {
137
+ const pipe = definePipe('test_pipe', {
138
+ nodes: [
139
+ node({ name: 'first', sql: 'SELECT * FROM table1' }),
140
+ node({ name: 'second', sql: 'SELECT * FROM first' }),
141
+ ],
142
+ output: simpleOutput,
143
+ endpoint: true,
144
+ });
145
+
146
+ const result = generatePipe(pipe);
147
+ expect(result.content).toContain('NODE first');
148
+ expect(result.content).toContain('NODE second');
149
+ expect(result.content).toContain('SELECT * FROM table1');
150
+ expect(result.content).toContain('SELECT * FROM first');
151
+ });
152
+
153
+ it('includes node descriptions', () => {
154
+ const pipe = definePipe('test_pipe', {
155
+ nodes: [
156
+ node({
157
+ name: 'endpoint',
158
+ description: 'This is a test node',
159
+ sql: 'SELECT 1',
160
+ }),
161
+ ],
162
+ output: simpleOutput,
163
+ endpoint: true,
164
+ });
165
+
166
+ const result = generatePipe(pipe);
167
+ expect(result.content).toContain('NODE endpoint');
168
+ expect(result.content).toContain('DESCRIPTION >');
169
+ expect(result.content).toContain('This is a test node');
170
+ });
171
+ });
172
+
173
+ describe('Endpoint configuration', () => {
174
+ it('includes cache when enabled', () => {
175
+ const pipe = definePipe('test_pipe', {
176
+ nodes: [node({ name: 'endpoint', sql: 'SELECT 1' })],
177
+ output: simpleOutput,
178
+ endpoint: {
179
+ enabled: true,
180
+ cache: { enabled: true, ttl: 300 },
181
+ },
182
+ });
183
+
184
+ const result = generatePipe(pipe);
185
+ expect(result.content).toContain('TYPE endpoint');
186
+ expect(result.content).toContain('CACHE 300');
187
+ });
188
+
189
+ it('uses default cache TTL when not specified', () => {
190
+ const pipe = definePipe('test_pipe', {
191
+ nodes: [node({ name: 'endpoint', sql: 'SELECT 1' })],
192
+ output: simpleOutput,
193
+ endpoint: {
194
+ enabled: true,
195
+ cache: { enabled: true },
196
+ },
197
+ });
198
+
199
+ const result = generatePipe(pipe);
200
+ expect(result.content).toContain('CACHE 60');
201
+ });
202
+ });
203
+
204
+ describe('generateAllPipes', () => {
205
+ it('generates all pipes', () => {
206
+ const pipe1 = definePipe('pipe1', {
207
+ nodes: [node({ name: 'endpoint', sql: 'SELECT 1' })],
208
+ output: simpleOutput,
209
+ endpoint: true,
210
+ });
211
+ const pipe2 = definePipe('pipe2', {
212
+ nodes: [node({ name: 'endpoint', sql: 'SELECT 2' })],
213
+ output: simpleOutput,
214
+ endpoint: true,
215
+ });
216
+
217
+ const results = generateAllPipes({ pipe1, pipe2 });
218
+ expect(results).toHaveLength(2);
219
+ expect(results.map(r => r.name).sort()).toEqual(['pipe1', 'pipe2']);
220
+ });
221
+ });
222
+
223
+ describe('Full integration', () => {
224
+ it('generates complete pipe file', () => {
225
+ const pipe = definePipe('top_pages', {
226
+ description: 'Get the most visited pages',
227
+ params: {
228
+ start_date: p.dateTime(),
229
+ end_date: p.dateTime(),
230
+ limit: p.int32().optional(10),
231
+ },
232
+ nodes: [
233
+ node({
234
+ name: 'aggregated',
235
+ sql: `
236
+ SELECT
237
+ pathname,
238
+ count() AS views
239
+ FROM page_views
240
+ WHERE timestamp >= {{DateTime(start_date)}}
241
+ AND timestamp <= {{DateTime(end_date)}}
242
+ GROUP BY pathname
243
+ ORDER BY views DESC
244
+ LIMIT {{Int32(limit, 10)}}
245
+ `.trim(),
246
+ }),
247
+ ],
248
+ output: {
249
+ pathname: t.string(),
250
+ views: t.uint64(),
251
+ },
252
+ endpoint: true,
253
+ });
254
+
255
+ const result = generatePipe(pipe);
256
+
257
+ expect(result.name).toBe('top_pages');
258
+ expect(result.content).toContain('DESCRIPTION >');
259
+ expect(result.content).toContain('Get the most visited pages');
260
+ expect(result.content).toContain('NODE aggregated');
261
+ expect(result.content).toContain('SQL >');
262
+ expect(result.content).toContain(' %\n');
263
+ expect(result.content).toContain('pathname');
264
+ expect(result.content).toContain('{{DateTime(start_date)}}');
265
+ expect(result.content).toContain('{{Int32(limit, 10)}}');
266
+ expect(result.content).toContain('TYPE endpoint');
267
+ });
268
+ });
269
+
270
+ describe('Materialized Views', () => {
271
+ const salesByHour = defineDatasource('sales_by_hour', {
272
+ schema: {
273
+ day: t.date(),
274
+ country: t.string().lowCardinality(),
275
+ total_sales: t.simpleAggregateFunction('sum', t.uint64()),
276
+ },
277
+ engine: engine.aggregatingMergeTree({
278
+ sortingKey: ['day', 'country'],
279
+ }),
280
+ });
281
+
282
+ it('generates TYPE MATERIALIZED and DATASOURCE with datasource', () => {
283
+ const pipe = definePipe('sales_by_hour_mv', {
284
+ nodes: [
285
+ node({
286
+ name: 'daily_sales',
287
+ sql: 'SELECT toStartOfDay(date) as day, country, sum(sales) as total_sales FROM teams GROUP BY day, country',
288
+ }),
289
+ ],
290
+ output: {
291
+ day: t.date(),
292
+ country: t.string().lowCardinality(),
293
+ total_sales: t.simpleAggregateFunction('sum', t.uint64()),
294
+ },
295
+ materialized: {
296
+ datasource: salesByHour,
297
+ },
298
+ });
299
+
300
+ const result = generatePipe(pipe);
301
+
302
+ expect(result.content).toContain('TYPE MATERIALIZED');
303
+ expect(result.content).toContain('DATASOURCE sales_by_hour');
304
+ expect(result.content).not.toContain('TYPE endpoint');
305
+ });
306
+
307
+ it('generates DEPLOYMENT_METHOD alter when specified', () => {
308
+ const pipe = definePipe('sales_by_hour_mv', {
309
+ nodes: [
310
+ node({
311
+ name: 'daily_sales',
312
+ sql: 'SELECT toStartOfDay(date) as day, country, sum(sales) as total_sales FROM teams GROUP BY day, country',
313
+ }),
314
+ ],
315
+ output: {
316
+ day: t.date(),
317
+ country: t.string().lowCardinality(),
318
+ total_sales: t.simpleAggregateFunction('sum', t.uint64()),
319
+ },
320
+ materialized: {
321
+ datasource: salesByHour,
322
+ deploymentMethod: 'alter',
323
+ },
324
+ });
325
+
326
+ const result = generatePipe(pipe);
327
+
328
+ expect(result.content).toContain('TYPE MATERIALIZED');
329
+ expect(result.content).toContain('DATASOURCE sales_by_hour');
330
+ expect(result.content).toContain('DEPLOYMENT_METHOD alter');
331
+ });
332
+
333
+ it('does not include DEPLOYMENT_METHOD when not specified', () => {
334
+ const pipe = definePipe('sales_by_hour_mv', {
335
+ nodes: [
336
+ node({
337
+ name: 'daily_sales',
338
+ sql: 'SELECT toStartOfDay(date) as day, country, sum(sales) as total_sales FROM teams GROUP BY day, country',
339
+ }),
340
+ ],
341
+ output: {
342
+ day: t.date(),
343
+ country: t.string().lowCardinality(),
344
+ total_sales: t.simpleAggregateFunction('sum', t.uint64()),
345
+ },
346
+ materialized: {
347
+ datasource: salesByHour,
348
+ },
349
+ });
350
+
351
+ const result = generatePipe(pipe);
352
+
353
+ expect(result.content).not.toContain('DEPLOYMENT_METHOD');
354
+ });
355
+
356
+ it('generates complete materialized view pipe file', () => {
357
+ const pipe = definePipe('sales_by_hour_mv', {
358
+ description: 'Aggregate sales per hour',
359
+ nodes: [
360
+ node({
361
+ name: 'daily_sales',
362
+ sql: `
363
+ SELECT
364
+ toStartOfDay(starting_date) as day,
365
+ country,
366
+ sum(sales) as total_sales
367
+ FROM teams
368
+ GROUP BY day, country
369
+ `.trim(),
370
+ }),
371
+ ],
372
+ output: {
373
+ day: t.date(),
374
+ country: t.string().lowCardinality(),
375
+ total_sales: t.simpleAggregateFunction('sum', t.uint64()),
376
+ },
377
+ materialized: {
378
+ datasource: salesByHour,
379
+ deploymentMethod: 'alter',
380
+ },
381
+ });
382
+
383
+ const result = generatePipe(pipe);
384
+
385
+ expect(result.name).toBe('sales_by_hour_mv');
386
+ expect(result.content).toContain('DESCRIPTION >');
387
+ expect(result.content).toContain('Aggregate sales per hour');
388
+ expect(result.content).toContain('NODE daily_sales');
389
+ expect(result.content).toContain('SQL >');
390
+ expect(result.content).toContain('toStartOfDay(starting_date) as day');
391
+ expect(result.content).toContain('TYPE MATERIALIZED');
392
+ expect(result.content).toContain('DATASOURCE sales_by_hour');
393
+ expect(result.content).toContain('DEPLOYMENT_METHOD alter');
394
+ });
395
+
396
+ it('works with defineMaterializedView helper using datasource', () => {
397
+ const pipe = defineMaterializedView('sales_mv', {
398
+ description: 'Sales materialized view',
399
+ datasource: salesByHour,
400
+ nodes: [
401
+ node({
402
+ name: 'daily_sales',
403
+ sql: 'SELECT toStartOfDay(date) as day, country, sum(sales) as total_sales FROM events GROUP BY day, country',
404
+ }),
405
+ ],
406
+ deploymentMethod: 'alter',
407
+ });
408
+
409
+ const result = generatePipe(pipe);
410
+
411
+ expect(result.name).toBe('sales_mv');
412
+ expect(result.content).toContain('TYPE MATERIALIZED');
413
+ expect(result.content).toContain('DATASOURCE sales_by_hour');
414
+ expect(result.content).toContain('DEPLOYMENT_METHOD alter');
415
+ });
416
+
417
+ it('generates DATASOURCE correctly with datasource field', () => {
418
+ const pipe = definePipe('sales_by_hour_mv_2', {
419
+ nodes: [
420
+ node({
421
+ name: 'daily_sales',
422
+ sql: 'SELECT toStartOfDay(date) as day, country, sum(sales) as total_sales FROM teams GROUP BY day, country',
423
+ }),
424
+ ],
425
+ output: {
426
+ day: t.date(),
427
+ country: t.string().lowCardinality(),
428
+ total_sales: t.simpleAggregateFunction('sum', t.uint64()),
429
+ },
430
+ materialized: {
431
+ datasource: salesByHour,
432
+ },
433
+ });
434
+
435
+ const result = generatePipe(pipe);
436
+
437
+ expect(result.content).toContain('TYPE MATERIALIZED');
438
+ expect(result.content).toContain('DATASOURCE sales_by_hour');
439
+ });
440
+ });
441
+ });
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Pipe content generator
3
+ * Converts PipeDefinition to native .pipe file format
4
+ */
5
+
6
+ import type {
7
+ PipeDefinition,
8
+ NodeDefinition,
9
+ EndpointConfig,
10
+ MaterializedConfig,
11
+ CopyConfig,
12
+ } from "../schema/pipe.js";
13
+ import { getEndpointConfig, getMaterializedConfig, getCopyConfig } from "../schema/pipe.js";
14
+
15
+ /**
16
+ * Generated pipe content
17
+ */
18
+ export interface GeneratedPipe {
19
+ /** Pipe name */
20
+ name: string;
21
+ /** The generated .pipe file content */
22
+ content: string;
23
+ }
24
+
25
+ /**
26
+ * Check if SQL contains template parameters like {{...}}
27
+ */
28
+ function hasDynamicParameters(sql: string): boolean {
29
+ return /\{\{[^}]+\}\}/.test(sql);
30
+ }
31
+
32
+ /**
33
+ * Generate a NODE section for the pipe
34
+ */
35
+ function generateNode(node: NodeDefinition): string {
36
+ const parts: string[] = [];
37
+
38
+ parts.push(`NODE ${node._name}`);
39
+
40
+ if (node.description) {
41
+ parts.push(`DESCRIPTION >`);
42
+ parts.push(` ${node.description}`);
43
+ }
44
+
45
+ parts.push(`SQL >`);
46
+
47
+ // Check if SQL has dynamic parameters - if so, add % on its own line
48
+ const isDynamic = hasDynamicParameters(node.sql);
49
+ if (isDynamic) {
50
+ parts.push(` %`);
51
+ }
52
+
53
+ const sqlLines = node.sql.trim().split("\n");
54
+ sqlLines.forEach((line) => {
55
+ parts.push(` ${line}`);
56
+ });
57
+
58
+ return parts.join("\n");
59
+ }
60
+
61
+ /**
62
+ * Generate the TYPE endpoint section
63
+ */
64
+ function generateEndpoint(endpoint: EndpointConfig): string {
65
+ const parts: string[] = ["TYPE endpoint"];
66
+
67
+ if (endpoint.cache?.enabled) {
68
+ if (endpoint.cache.ttl !== undefined) {
69
+ parts.push(`CACHE ${endpoint.cache.ttl}`);
70
+ } else {
71
+ parts.push("CACHE 60"); // Default cache TTL
72
+ }
73
+ }
74
+
75
+ return parts.join("\n");
76
+ }
77
+
78
+ /**
79
+ * Generate the TYPE MATERIALIZED section
80
+ */
81
+ function generateMaterialized(config: MaterializedConfig): string {
82
+ const parts: string[] = ["TYPE MATERIALIZED"];
83
+
84
+ // The config is normalized by definePipe to always have `datasource` set.
85
+ // Use non-null assertion since we know it's always present after normalization.
86
+ const datasourceName = config.datasource!._name;
87
+ parts.push(`DATASOURCE ${datasourceName}`);
88
+
89
+ if (config.deploymentMethod === "alter") {
90
+ parts.push("DEPLOYMENT_METHOD alter");
91
+ }
92
+
93
+ return parts.join("\n");
94
+ }
95
+
96
+ /**
97
+ * Generate the TYPE COPY section
98
+ */
99
+ function generateCopy(config: CopyConfig): string {
100
+ const parts: string[] = ["TYPE COPY"];
101
+
102
+ const datasourceName = config.datasource._name;
103
+ parts.push(`TARGET_DATASOURCE ${datasourceName}`);
104
+
105
+ if (config.copy_schedule) {
106
+ parts.push(`COPY_SCHEDULE ${config.copy_schedule}`);
107
+ }
108
+
109
+ if (config.copy_mode) {
110
+ parts.push(`COPY_MODE ${config.copy_mode}`);
111
+ }
112
+
113
+ return parts.join("\n");
114
+ }
115
+
116
+ /**
117
+ * Generate a .pipe file content from a PipeDefinition
118
+ *
119
+ * @param pipe - The pipe definition
120
+ * @returns Generated pipe content
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * const topEvents = definePipe('top_events', {
125
+ * description: 'Get top events by count',
126
+ * params: {
127
+ * start_date: p.dateTime(),
128
+ * limit: p.int32().optional(10),
129
+ * },
130
+ * nodes: [
131
+ * node({
132
+ * name: 'endpoint',
133
+ * sql: `
134
+ * SELECT event_type, count() as count
135
+ * FROM events
136
+ * WHERE timestamp >= {{DateTime(start_date)}}
137
+ * ORDER BY count DESC
138
+ * LIMIT {{Int32(limit, 10)}}
139
+ * `,
140
+ * }),
141
+ * ],
142
+ * output: {
143
+ * event_type: t.string(),
144
+ * count: t.uint64(),
145
+ * },
146
+ * endpoint: true,
147
+ * });
148
+ *
149
+ * const { content } = generatePipe(topEvents);
150
+ * // Returns:
151
+ * // DESCRIPTION >
152
+ * // Get top events by count
153
+ * //
154
+ * // NODE endpoint
155
+ * // SQL >
156
+ * // SELECT event_type, count() as count
157
+ * // FROM events
158
+ * // WHERE timestamp >= {{DateTime(start_date)}}
159
+ * // ORDER BY count DESC
160
+ * // LIMIT {{Int32(limit, 10)}}
161
+ * //
162
+ * // TYPE endpoint
163
+ * ```
164
+ */
165
+ export function generatePipe(pipe: PipeDefinition): GeneratedPipe {
166
+ const parts: string[] = [];
167
+
168
+ // Add description if present
169
+ if (pipe.options.description) {
170
+ parts.push(`DESCRIPTION >\n ${pipe.options.description}`);
171
+ parts.push("");
172
+ }
173
+
174
+ // Add all nodes
175
+ pipe.options.nodes.forEach((node, index) => {
176
+ parts.push(generateNode(node));
177
+ // Add empty line between nodes
178
+ if (index < pipe.options.nodes.length - 1) {
179
+ parts.push("");
180
+ }
181
+ });
182
+
183
+ // Add endpoint configuration if this is an endpoint
184
+ const endpointConfig = getEndpointConfig(pipe);
185
+ if (endpointConfig) {
186
+ parts.push("");
187
+ parts.push(generateEndpoint(endpointConfig));
188
+ }
189
+
190
+ // Add materialized view configuration if this is a materialized view
191
+ const materializedConfig = getMaterializedConfig(pipe);
192
+ if (materializedConfig) {
193
+ parts.push("");
194
+ parts.push(generateMaterialized(materializedConfig));
195
+ }
196
+
197
+ // Add copy pipe configuration if this is a copy pipe
198
+ const copyConfig = getCopyConfig(pipe);
199
+ if (copyConfig) {
200
+ parts.push("");
201
+ parts.push(generateCopy(copyConfig));
202
+ }
203
+
204
+ return {
205
+ name: pipe._name,
206
+ content: parts.join("\n"),
207
+ };
208
+ }
209
+
210
+ /**
211
+ * Generate .pipe files for all pipes in a project
212
+ *
213
+ * @param pipes - Record of pipe definitions
214
+ * @returns Array of generated pipe content
215
+ */
216
+ export function generateAllPipes(
217
+ pipes: Record<string, PipeDefinition>
218
+ ): GeneratedPipe[] {
219
+ return Object.values(pipes).map(generatePipe);
220
+ }