@malloy-publisher/server 0.0.226 → 0.0.228

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 (112) hide show
  1. package/README.docker.md +8 -8
  2. package/README.md +2 -11
  3. package/dist/app/assets/{EnvironmentPage-DvOJ7L_b.js → EnvironmentPage-EW2lbGvb.js} +1 -1
  4. package/dist/app/assets/{HomePage-CXguJsXS.js → HomePage-Bkwc9Woc.js} +1 -1
  5. package/dist/app/assets/{LightMode-ZsshUznu.js → LightMode-Bum_KBpN.js} +1 -1
  6. package/dist/app/assets/{MainPage-BIe0VwBa.js → MainPage-oiEy7TNM.js} +1 -1
  7. package/dist/app/assets/{MaterializationsPage-BuZ6UJVx.js → MaterializationsPage-C_VJsTgU.js} +1 -1
  8. package/dist/app/assets/{ModelPage-DsPf-s8B.js → ModelPage-z8REqAmk.js} +1 -1
  9. package/dist/app/assets/{PackagePage-CEVNAKZa.js → PackagePage-C2Vtt1Ln.js} +1 -1
  10. package/dist/app/assets/{RouteError-Chn7lL96.js → RouteError-DmJLpLXm.js} +1 -1
  11. package/dist/app/assets/{ThemeEditorPage-DWC_FdNU.js → ThemeEditorPage-BywFjC7A.js} +1 -1
  12. package/dist/app/assets/{WorkbookPage-CGrsFz8p.js → WorkbookPage-DCMizDMR.js} +1 -1
  13. package/dist/app/assets/{core-vVgoO8IR.es-BD_THWs_.js → core-CEDZMHV1.es-_yGzNgNH.js} +1 -1
  14. package/dist/app/assets/{index-D6YtyiJ0.js → index-CE9xhdra.js} +1 -1
  15. package/dist/app/assets/{index-BioohWQj.js → index-CdmFub34.js} +1 -1
  16. package/dist/app/assets/{index-gEWxu09x.js → index-DDMrjIT3.js} +1 -1
  17. package/dist/app/assets/{index-DNUZpnaa.js → index-EqslXZ44.js} +4 -4
  18. package/dist/app/index.html +1 -1
  19. package/dist/default-publisher.config.json +7 -7
  20. package/dist/package_load_worker.mjs +86 -24
  21. package/dist/runtime/publisher.js +5 -0
  22. package/dist/server.mjs +1415 -7876
  23. package/package.json +1 -4
  24. package/publisher.config.example.bigquery.json +7 -7
  25. package/publisher.config.example.duckdb.json +7 -7
  26. package/publisher.config.json +7 -11
  27. package/src/config.spec.ts +2 -2
  28. package/src/controller/package.controller.ts +62 -31
  29. package/src/default-publisher.config.json +7 -7
  30. package/src/health.ts +3 -8
  31. package/src/mcp/error_messages.ts +8 -37
  32. package/src/mcp/handler_utils.spec.ts +108 -0
  33. package/src/mcp/handler_utils.ts +99 -124
  34. package/src/mcp/mcp_constants.ts +0 -16
  35. package/src/mcp/server.protocol.spec.ts +138 -0
  36. package/src/mcp/server.ts +57 -37
  37. package/src/mcp/skills/build_skills_bundle.ts +1 -1
  38. package/src/mcp/skills/skills_bundle.json +1 -1
  39. package/src/mcp/tools/compile_tool.spec.ts +207 -0
  40. package/src/mcp/tools/compile_tool.ts +177 -0
  41. package/src/mcp/tools/docs_search_tool.ts +1 -1
  42. package/src/mcp/tools/execute_query_tool.spec.ts +143 -0
  43. package/src/mcp/tools/execute_query_tool.ts +39 -6
  44. package/src/mcp/tools/get_context_eval.ts +3 -3
  45. package/src/mcp/tools/get_context_tool.spec.ts +196 -1
  46. package/src/mcp/tools/get_context_tool.ts +165 -67
  47. package/src/mcp/tools/reload_package_tool.spec.ts +232 -0
  48. package/src/mcp/tools/reload_package_tool.ts +158 -0
  49. package/src/package_load/package_load_pool.ts +3 -0
  50. package/src/package_load/package_load_worker.ts +68 -24
  51. package/src/package_load/protocol.ts +16 -0
  52. package/src/package_load/rpc_wait_accountant.spec.ts +109 -0
  53. package/src/package_load/rpc_wait_accountant.ts +76 -0
  54. package/src/package_load_metrics.spec.ts +114 -0
  55. package/src/package_load_metrics.ts +127 -0
  56. package/src/pg_helpers.spec.ts +2 -206
  57. package/src/pg_helpers.ts +4 -120
  58. package/src/runtime/publisher.js +5 -0
  59. package/src/server.ts +7 -21
  60. package/src/service/environment.ts +71 -7
  61. package/src/service/model.spec.ts +92 -0
  62. package/src/service/model.ts +58 -7
  63. package/src/service/package.ts +113 -55
  64. package/src/service/package_reload_safety.spec.ts +193 -0
  65. package/src/test_helpers/metrics_harness.ts +40 -0
  66. package/tests/fixtures/query-givens/data/orders.csv +7 -0
  67. package/tests/fixtures/query-givens/model.malloy +34 -0
  68. package/tests/fixtures/query-givens/publisher.json +5 -0
  69. package/tests/harness/mcp_test_setup.ts +1 -1
  70. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +22 -22
  71. package/tests/integration/mcp/mcp_transport.integration.spec.ts +7 -31
  72. package/tests/integration/query_givens/query_givens.integration.spec.ts +146 -0
  73. package/tests/integration/query_givens/query_givens_authorize.integration.spec.ts +121 -0
  74. package/tests/integration/sdk_givens/sdk_givens.integration.spec.ts +110 -0
  75. package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -6
  76. package/dxt/malloy_bridge.py +0 -354
  77. package/dxt/manifest.json +0 -22
  78. package/src/dto/connection.dto.spec.ts +0 -186
  79. package/src/dto/connection.dto.ts +0 -308
  80. package/src/dto/index.ts +0 -2
  81. package/src/dto/package.dto.spec.ts +0 -42
  82. package/src/dto/package.dto.ts +0 -27
  83. package/src/dto/validate.spec.ts +0 -76
  84. package/src/dto/validate.ts +0 -31
  85. package/src/ducklake_version.spec.ts +0 -43
  86. package/src/ducklake_version.ts +0 -26
  87. package/src/mcp/agent_server.protocol.spec.ts +0 -78
  88. package/src/mcp/agent_server.spec.ts +0 -18
  89. package/src/mcp/agent_server.ts +0 -144
  90. package/src/mcp/prompts/handlers.ts +0 -84
  91. package/src/mcp/prompts/index.ts +0 -11
  92. package/src/mcp/prompts/prompt_definitions.ts +0 -160
  93. package/src/mcp/prompts/prompt_service.ts +0 -67
  94. package/src/mcp/prompts/utils.ts +0 -62
  95. package/src/mcp/resource_metadata.ts +0 -47
  96. package/src/mcp/resources/environment_resource.ts +0 -187
  97. package/src/mcp/resources/model_resource.ts +0 -155
  98. package/src/mcp/resources/notebook_resource.ts +0 -137
  99. package/src/mcp/resources/package_resource.ts +0 -373
  100. package/src/mcp/resources/query_resource.ts +0 -122
  101. package/src/mcp/resources/source_resource.ts +0 -141
  102. package/src/mcp/resources/view_resource.ts +0 -136
  103. package/src/mcp/tools/discovery_tools.ts +0 -280
  104. package/src/storage/BaseRepository.ts +0 -31
  105. package/src/storage/StorageManager.mock.ts +0 -50
  106. package/tests/harness/e2e.ts +0 -96
  107. package/tests/harness/mocks.ts +0 -39
  108. package/tests/harness/uris.ts +0 -31
  109. package/tests/integration/mcp/mcp_resource.integration.spec.ts +0 -655
  110. package/tests/integration/mcp/setup.spec.ts +0 -5
  111. package/tests/unit/mcp/prompt_definitions.test.ts +0 -102
  112. package/tests/unit/mcp/prompt_happy.test.ts +0 -51
@@ -1,186 +0,0 @@
1
- import { expect, it, describe } from "bun:test";
2
-
3
- import { plainToInstance } from "class-transformer";
4
- import { validate } from "class-validator";
5
- import {
6
- BigqueryConnectionDto,
7
- ConnectionDto,
8
- DatabricksConnectionDto,
9
- PostgresConnectionDto,
10
- SnowflakeConnectionDto,
11
- MysqlConnectionDto,
12
- } from "./connection.dto";
13
-
14
- describe("dto/connection", () => {
15
- describe("Connection Validation", () => {
16
- it("should validate a valid MysqlConnection object", async () => {
17
- const validData = {
18
- host: "localhost",
19
- port: 3306,
20
- database: "testdb",
21
- user: "user",
22
- password: "pass",
23
- };
24
- const mysqlConnection = plainToInstance(MysqlConnectionDto, validData);
25
-
26
- const errors = await validate(mysqlConnection);
27
- expect(errors).toHaveLength(0);
28
- });
29
-
30
- it("should validate a valid PostgresConnection object", async () => {
31
- const validData = {
32
- host: "localhost",
33
- port: 5432,
34
- databaseName: "testdb",
35
- userName: "user",
36
- password: "pass",
37
- connectionString: "postgres://user:pass@localhost:5432/testdb",
38
- };
39
- const postgresConnection = plainToInstance(
40
- PostgresConnectionDto,
41
- validData,
42
- );
43
-
44
- const errors = await validate(postgresConnection);
45
- expect(errors).toHaveLength(0);
46
- });
47
-
48
- it("should return errors for invalid PostgresConnection object", async () => {
49
- const invalidData = {
50
- host: 123, // Invalid type
51
- port: "not-a-number", // Invalid type
52
- };
53
- const postgresConnection = plainToInstance(
54
- PostgresConnectionDto,
55
- invalidData,
56
- );
57
-
58
- const errors = await validate(postgresConnection);
59
- expect(errors).not.toHaveLength(0);
60
- expect(errors).toHaveLength(2);
61
- });
62
-
63
- it("should validate a valid BigqueryConnection object", async () => {
64
- const validData = {
65
- defaultProjectId: "default-project",
66
- billingProjectId: "billing-project",
67
- location: "US",
68
- serviceAccountKeyJson: "{}",
69
- maximumBytesBilled: "1000000",
70
- queryTimeoutMilliseconds: "1000",
71
- };
72
- const bigqueryConnection = plainToInstance(
73
- BigqueryConnectionDto,
74
- validData,
75
- );
76
-
77
- const errors = await validate(bigqueryConnection);
78
- expect(errors).toHaveLength(0);
79
- });
80
-
81
- it("should validate a valid SnowflakeConnection object", async () => {
82
- const validData = {
83
- account: "my-account",
84
- username: "user",
85
- password: "pass",
86
- privateKey:
87
- "-----BEGIN PRIVATE KEY-----\\nabc\\n-----END PRIVATE KEY-----",
88
- privateKeyPass: "secret",
89
- warehouse: "my-warehouse",
90
- database: "my-database",
91
- schema: "my-schema",
92
- role: "analyst",
93
- responseTimeoutMilliseconds: 5000,
94
- };
95
- const snowflakeConnection = plainToInstance(
96
- SnowflakeConnectionDto,
97
- validData,
98
- );
99
-
100
- const errors = await validate(snowflakeConnection);
101
- expect(errors).toHaveLength(0);
102
- });
103
-
104
- it("should validate a valid DatabricksConnection object", async () => {
105
- const validData = {
106
- host: "dbc-xxxxxxxx-xxxx.cloud.databricks.com",
107
- path: "/sql/1.0/warehouses/abc123",
108
- token: "dapiXXXX",
109
- oauthClientId: "client-id",
110
- oauthClientSecret: "client-secret",
111
- defaultCatalog: "main",
112
- defaultSchema: "default",
113
- setupSQL: "USE CATALOG main",
114
- };
115
- const databricksConnection = plainToInstance(
116
- DatabricksConnectionDto,
117
- validData,
118
- );
119
-
120
- const errors = await validate(databricksConnection);
121
- expect(errors).toHaveLength(0);
122
- });
123
-
124
- it("should return errors for invalid DatabricksConnection object", async () => {
125
- const invalidData = {
126
- host: 123, // Invalid type
127
- path: false, // Invalid type
128
- };
129
- const databricksConnection = plainToInstance(
130
- DatabricksConnectionDto,
131
- invalidData,
132
- );
133
-
134
- const errors = await validate(databricksConnection);
135
- expect(errors).toHaveLength(2);
136
- });
137
-
138
- it("should validate a valid Connection object with databricks type", async () => {
139
- const validData = {
140
- name: "My Databricks Connection",
141
- type: "databricks",
142
- databricksConnection: {
143
- host: "dbc-xxxxxxxx-xxxx.cloud.databricks.com",
144
- path: "/sql/1.0/warehouses/abc123",
145
- token: "dapiXXXX",
146
- },
147
- };
148
- const connection = plainToInstance(ConnectionDto, validData);
149
-
150
- const errors = await validate(connection);
151
- expect(errors).toHaveLength(0);
152
- });
153
-
154
- it("should validate a valid Connection object with postgres type", async () => {
155
- const validData = {
156
- name: "My Postgres Connection",
157
- type: "postgres",
158
- postgresConnection: {
159
- host: "localhost",
160
- port: 5432,
161
- databaseName: "testdb",
162
- userName: "user",
163
- password: "pass",
164
- },
165
- };
166
- const connection = plainToInstance(ConnectionDto, validData);
167
-
168
- const errors = await validate(connection);
169
- expect(errors).toHaveLength(0);
170
- });
171
-
172
- it("should return errors for invalid Connection object", async () => {
173
- const invalidData = {
174
- type: "invalid-type", // Invalid enum value
175
- postgresConnection: {
176
- port: "invalid-port", // Invalid type
177
- },
178
- };
179
- const connection = plainToInstance(ConnectionDto, invalidData);
180
-
181
- const errors = await validate(connection);
182
- expect(errors).not.toHaveLength(0);
183
- expect(errors.length).toBeGreaterThan(0);
184
- });
185
- });
186
- });
@@ -1,308 +0,0 @@
1
- import { Type } from "class-transformer";
2
- import {
3
- IsDefined,
4
- IsEnum,
5
- IsArray,
6
- IsNumber,
7
- IsOptional,
8
- IsString,
9
- ValidateIf,
10
- ValidateNested,
11
- } from "class-validator";
12
- import "reflect-metadata";
13
- import { components } from "../api";
14
- import { ApiConnection } from "../service/model";
15
-
16
- type AttachedDatabase = components["schemas"]["AttachedDatabase"];
17
-
18
- export class PostgresConnectionDto {
19
- @IsOptional()
20
- @IsString()
21
- host?: string;
22
-
23
- @IsOptional()
24
- @IsNumber()
25
- port?: number;
26
-
27
- @IsOptional()
28
- @IsString()
29
- databaseName?: string;
30
-
31
- @IsOptional()
32
- @IsString()
33
- userName?: string;
34
-
35
- @IsOptional()
36
- @IsString()
37
- password?: string;
38
-
39
- @IsOptional()
40
- @IsString()
41
- connectionString?: string;
42
- }
43
-
44
- export class MysqlConnectionDto {
45
- @IsOptional()
46
- @IsString()
47
- host?: string;
48
-
49
- @IsOptional()
50
- @IsNumber()
51
- port?: number;
52
-
53
- @IsOptional()
54
- @IsString()
55
- database?: string;
56
-
57
- @IsOptional()
58
- @IsString()
59
- user?: string;
60
-
61
- @IsOptional()
62
- @IsString()
63
- password?: string;
64
- }
65
-
66
- export class BigqueryConnectionDto {
67
- @IsOptional()
68
- @IsString()
69
- defaultProjectId?: string;
70
-
71
- @IsOptional()
72
- @IsString()
73
- billingProjectId?: string;
74
-
75
- @IsOptional()
76
- @IsString()
77
- location?: string;
78
-
79
- @IsOptional()
80
- @IsString()
81
- serviceAccountKeyJson?: string;
82
-
83
- @IsOptional()
84
- @IsString()
85
- maximumBytesBilled?: string;
86
-
87
- @IsOptional()
88
- @IsString()
89
- queryTimeoutMilliseconds?: string;
90
- }
91
-
92
- export class SnowflakeConnectionDto {
93
- @IsOptional()
94
- @IsString()
95
- account?: string;
96
-
97
- @IsOptional()
98
- @IsString()
99
- username?: string;
100
-
101
- @IsOptional()
102
- @IsString()
103
- password?: string;
104
-
105
- @IsOptional()
106
- @IsString()
107
- privateKey?: string;
108
-
109
- @IsOptional()
110
- @IsString()
111
- privateKeyPass?: string;
112
-
113
- @IsOptional()
114
- @IsString()
115
- warehouse?: string;
116
-
117
- @IsOptional()
118
- @IsString()
119
- database?: string;
120
-
121
- @IsOptional()
122
- @IsString()
123
- schema?: string;
124
-
125
- @IsOptional()
126
- @IsString()
127
- role?: string;
128
-
129
- @IsOptional()
130
- @IsNumber()
131
- responseTimeoutMilliseconds?: number;
132
- }
133
-
134
- export class DatabricksConnectionDto {
135
- @IsOptional()
136
- @IsString()
137
- host?: string;
138
-
139
- @IsOptional()
140
- @IsString()
141
- path?: string;
142
-
143
- @IsOptional()
144
- @IsString()
145
- token?: string;
146
-
147
- @IsOptional()
148
- @IsString()
149
- oauthClientId?: string;
150
-
151
- @IsOptional()
152
- @IsString()
153
- oauthClientSecret?: string;
154
-
155
- @IsOptional()
156
- @IsString()
157
- defaultCatalog?: string;
158
-
159
- @IsOptional()
160
- @IsString()
161
- defaultSchema?: string;
162
-
163
- @IsOptional()
164
- @IsString()
165
- setupSQL?: string;
166
- }
167
-
168
- export class TrinoConnectionDto {
169
- @IsOptional()
170
- @IsString()
171
- server?: string;
172
-
173
- @IsOptional()
174
- @IsString()
175
- port?: number;
176
-
177
- @IsOptional()
178
- @IsString()
179
- catalog?: string;
180
-
181
- @IsOptional()
182
- @IsString()
183
- schema?: string;
184
-
185
- @IsOptional()
186
- @IsString()
187
- user?: string;
188
-
189
- @IsOptional()
190
- @IsString()
191
- password?: string;
192
-
193
- @IsOptional()
194
- @IsString()
195
- peakaKey?: string;
196
- }
197
-
198
- export class DuckdbConnectionDto {
199
- @IsOptional()
200
- @IsArray()
201
- attachedDatabases?: AttachedDatabase[];
202
- }
203
-
204
- export class SshProxyConfigDto {
205
- @IsString()
206
- host!: string;
207
-
208
- @IsOptional()
209
- @IsNumber()
210
- port?: number;
211
-
212
- @IsString()
213
- username!: string;
214
-
215
- @IsString()
216
- privateKey!: string;
217
-
218
- @IsOptional()
219
- @IsString()
220
- privateKeyPass?: string;
221
-
222
- @IsOptional()
223
- @IsString()
224
- hostKey?: string;
225
- }
226
-
227
- export class ConnectionProxyDto {
228
- @IsEnum(["ssh"])
229
- type!: "ssh";
230
-
231
- // ssh config is required for the ssh proxy type (the only type today); keep
232
- // the conditional so a future proxy type isn't forced to supply `ssh`.
233
- @ValidateIf((o) => o.type === "ssh")
234
- @IsDefined()
235
- @ValidateNested()
236
- @Type(() => SshProxyConfigDto)
237
- ssh?: SshProxyConfigDto;
238
- }
239
-
240
- export class ConnectionDto implements ApiConnection {
241
- @IsOptional()
242
- @IsString()
243
- name?: string;
244
-
245
- @IsOptional()
246
- @IsEnum([
247
- "postgres",
248
- "bigquery",
249
- "snowflake",
250
- "trino",
251
- "databricks",
252
- "mysql",
253
- "duckdb",
254
- "motherduck",
255
- "ducklake",
256
- ])
257
- type?:
258
- | "postgres"
259
- | "bigquery"
260
- | "snowflake"
261
- | "trino"
262
- | "databricks"
263
- | "mysql"
264
- | "duckdb"
265
- | "motherduck"
266
- | "ducklake";
267
-
268
- // Opaque, secret-free data-identity token; carried verbatim (never parsed
269
- // or re-hashed) into content-addressed source ids. See api-doc.yaml.
270
- @IsOptional()
271
- @IsString()
272
- fingerprint?: string;
273
-
274
- @IsOptional()
275
- @ValidateNested()
276
- @Type(() => PostgresConnectionDto)
277
- postgresConnection?: PostgresConnectionDto;
278
-
279
- @IsOptional()
280
- @ValidateNested()
281
- @Type(() => BigqueryConnectionDto)
282
- bigqueryConnection?: BigqueryConnectionDto;
283
-
284
- @IsOptional()
285
- @ValidateNested()
286
- @Type(() => SnowflakeConnectionDto)
287
- snowflakeConnection?: SnowflakeConnectionDto;
288
-
289
- @IsOptional()
290
- @ValidateNested()
291
- @Type(() => TrinoConnectionDto)
292
- TrinoConnection?: TrinoConnectionDto;
293
-
294
- @IsOptional()
295
- @ValidateNested()
296
- @Type(() => DatabricksConnectionDto)
297
- databricksConnection?: DatabricksConnectionDto;
298
-
299
- @IsOptional()
300
- @ValidateNested()
301
- @Type(() => DuckdbConnectionDto)
302
- duckdbConnection?: DuckdbConnectionDto;
303
-
304
- @IsOptional()
305
- @ValidateNested()
306
- @Type(() => ConnectionProxyDto)
307
- proxy?: ConnectionProxyDto;
308
- }
package/src/dto/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from "./package.dto";
2
- export * from "./connection.dto";
@@ -1,42 +0,0 @@
1
- import { expect, it, describe } from "bun:test";
2
-
3
- import { plainToInstance } from "class-transformer";
4
- import { validate } from "class-validator";
5
- import { PackageDto } from "./package.dto";
6
- import { faker } from "@faker-js/faker";
7
-
8
- describe("dto/package", () => {
9
- it("should not throw when valid object is provided", async () => {
10
- const errors = await validate(
11
- plainToInstance(PackageDto, {
12
- name: faker.person.firstName(),
13
- description: faker.person.lastName(),
14
- }),
15
- );
16
-
17
- expect(errors).toHaveLength(0);
18
- });
19
-
20
- it("should throw when no name is specified", async () => {
21
- const errors = await validate(
22
- plainToInstance(PackageDto, {
23
- name: "",
24
- description: faker.person.lastName(),
25
- }),
26
- );
27
-
28
- expect(errors.length).toBeGreaterThan(0);
29
- expect(errors).toHaveLength(1);
30
- });
31
-
32
- it("should throw when no description is specified", async () => {
33
- const errors = await validate(
34
- plainToInstance(PackageDto, {
35
- name: faker.person.firstName(),
36
- }),
37
- );
38
-
39
- expect(errors.length).toBeGreaterThan(0);
40
- expect(errors).toHaveLength(1);
41
- });
42
- });
@@ -1,27 +0,0 @@
1
- import {
2
- IsString,
3
- IsNotEmpty,
4
- IsOptional,
5
- IsArray,
6
- IsIn,
7
- } from "class-validator";
8
- import { ApiPackage } from "../service/package";
9
-
10
- export class PackageDto implements ApiPackage {
11
- @IsString()
12
- @IsNotEmpty()
13
- name: string;
14
-
15
- @IsString()
16
- @IsNotEmpty()
17
- description: string;
18
-
19
- @IsOptional()
20
- @IsArray()
21
- @IsString({ each: true })
22
- explores?: string[];
23
-
24
- @IsOptional()
25
- @IsIn(["declared", "all"])
26
- queryableSources?: "declared" | "all";
27
- }
@@ -1,76 +0,0 @@
1
- import { expect, it, describe } from "bun:test";
2
- import request from "supertest";
3
- import express from "express";
4
- import { validateSchema } from "./validate";
5
- import { PackageDto } from "./package.dto";
6
- import { faker } from "@faker-js/faker";
7
-
8
- const app = express();
9
- app.use(express.json());
10
-
11
- app.post("/packages", validateSchema(PackageDto), (req, res) => {
12
- res.status(201).json({ message: "Package created", data: req.body });
13
- });
14
-
15
- describe("dto/validate", () => {
16
- describe("validateSchema", () => {
17
- it("should pass validation for a valid request", async () => {
18
- const dummyPayload = {
19
- name: faker.person.firstName(),
20
- description: faker.person.lastName(),
21
- };
22
-
23
- const response = await request(app)
24
- .post("/packages")
25
- .send(dummyPayload);
26
-
27
- expect(response.status).toBe(201);
28
- expect(response.body.message).toBe("Package created");
29
- expect(response.body.data).toEqual(dummyPayload);
30
- });
31
-
32
- it("should return 400 for missing required fields", async () => {
33
- const response = await request(app).post("/packages").send({
34
- name: "",
35
- description: "",
36
- });
37
-
38
- expect(response.status).toBe(400);
39
- expect(Array.isArray(response.body.errors)).toBe(true);
40
-
41
- const expectedErrors = [
42
- {
43
- property: "name",
44
- constraints: { isNotEmpty: "name should not be empty" },
45
- },
46
- {
47
- property: "description",
48
- constraints: {
49
- isNotEmpty: "description should not be empty",
50
- },
51
- },
52
- ];
53
- expect(response.body.errors).toEqual(
54
- expect.arrayContaining(expectedErrors),
55
- );
56
- });
57
-
58
- it("should return 400 for invalid description format", async () => {
59
- const response = await request(app).post("/packages").send({
60
- name: faker.person.firstName(),
61
- description: false,
62
- });
63
-
64
- expect(response.status).toBe(400);
65
- const expectedErrors = [
66
- {
67
- property: "description",
68
- constraints: { isString: "description must be a string" },
69
- },
70
- ];
71
- expect(response.body.errors).toEqual(
72
- expect.arrayContaining(expectedErrors),
73
- );
74
- });
75
- });
76
- });
@@ -1,31 +0,0 @@
1
- import { Request, Response, NextFunction } from "express";
2
- import { plainToInstance, ClassConstructor } from "class-transformer";
3
- import { validate } from "class-validator";
4
-
5
- /**
6
- * @description validates incoming requests based on the provided dto schema
7
- * @param dtoClass to validate against
8
- * @example
9
- * app.post('/users', validateSchema(UserDto), (req, res) => {
10
- * res.status(201).json({ message: 'User created', data: req.body });
11
- * });
12
- */
13
- export function validateSchema<T extends object>(
14
- dtoClass: ClassConstructor<T>,
15
- ) {
16
- return async (req: Request, res: Response, next: NextFunction) => {
17
- const dtoInstance = plainToInstance(dtoClass, req.body);
18
- const errors = await validate(dtoInstance);
19
-
20
- if (errors.length > 0) {
21
- const formattedErrors = errors.map((err) => ({
22
- property: err.property,
23
- constraints: err.constraints,
24
- }));
25
-
26
- return res.status(400).json({ errors: formattedErrors });
27
- }
28
-
29
- next();
30
- };
31
- }