@rebasepro/server-postgresql 0.6.0 → 0.6.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.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Patches all PgArray columns on the given tables to handle NULL values safely.
3
+ *
4
+ * Drizzle ORM's `PgArray.mapFromDriverValue` calls `value.map(...)` without
5
+ * guarding against `null`. When a PostgreSQL native array column (`text[]`,
6
+ * `integer[]`, etc.) contains NULL, the pg driver returns `null` in JavaScript,
7
+ * and `null.map(...)` throws `TypeError: value.map is not a function`.
8
+ *
9
+ * This function walks every column of every registered table and, for any
10
+ * `PgArray` column, wraps its `mapFromDriverValue` to return `null` when the
11
+ * database value is nullish.
12
+ *
13
+ * This is a workaround for a known Drizzle ORM issue. Should be removed once
14
+ * Drizzle handles nullable arrays natively.
15
+ */
16
+ export declare function patchPgArrayNullSafety(tables: Record<string, unknown>): void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/server-postgresql",
3
3
  "type": "module",
4
- "version": "0.6.0",
4
+ "version": "0.6.1",
5
5
  "description": "PostgreSQL data source backend implementation for Rebase with Drizzle ORM",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -71,11 +71,11 @@
71
71
  "hono": "^4.12.25",
72
72
  "pg": "^8.21.0",
73
73
  "ws": "^8.21.0",
74
- "@rebasepro/common": "0.6.0",
75
- "@rebasepro/types": "0.6.0",
76
- "@rebasepro/server-core": "0.6.0",
77
- "@rebasepro/sdk-generator": "0.6.0",
78
- "@rebasepro/utils": "0.6.0"
74
+ "@rebasepro/types": "0.6.1",
75
+ "@rebasepro/server-core": "0.6.1",
76
+ "@rebasepro/sdk-generator": "0.6.1",
77
+ "@rebasepro/common": "0.6.1",
78
+ "@rebasepro/utils": "0.6.1"
79
79
  },
80
80
  "devDependencies": {
81
81
  "@types/jest": "^30.0.0",
@@ -4,41 +4,31 @@
4
4
  * Implements the `BackendBootstrapper` interface for PostgreSQL.
5
5
  */
6
6
 
7
- import { getTableName, isTable, Relations, sql, Table } from "drizzle-orm";
7
+ import { getTableName, isTable, Relations, sql } from "drizzle-orm";
8
8
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
9
- import { PgEnum, PgTable, getTableConfig } from "drizzle-orm/pg-core";
9
+ import { PgEnum, PgTable } from "drizzle-orm/pg-core";
10
10
  import type { RebasePgTable } from "./types";
11
11
  import {
12
+ type AuthAdapter,
12
13
  BackendBootstrapper,
13
- InitializedDriver,
14
14
  BootstrappedAuth,
15
15
  DatabaseAdmin,
16
- RealtimeProvider,
17
16
  type DataDriver,
18
- type AuthAdapter,
19
17
  EntityCollection,
20
- PostgresCollection
18
+ InitializedDriver,
19
+ RealtimeProvider
21
20
  } from "@rebasepro/types";
22
21
  import { PostgresBackendDriver } from "./PostgresBackendDriver";
23
22
  import { RealtimeService } from "./services/realtimeService";
24
23
  import { DatabasePoolManager } from "./databasePoolManager";
25
24
  import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
26
- import {
27
- createAuthRoutes,
28
- requireAuth,
29
- requireAdmin,
30
- logger
31
- } from "@rebasepro/server-core";
25
+ import { createEmailService, type EmailConfig, type EmailService, logger } from "@rebasepro/server-core";
32
26
  import { ensureAuthTablesExist } from "./auth/ensure-tables";
33
- import { UserService, PostgresAuthRepository, AuthSchemaTables } from "./auth/services";
27
+ import { AuthSchemaTables, PostgresAuthRepository, UserService } from "./auth/services";
34
28
  import { createAuthSchema } from "./schema/auth-schema";
35
-
36
- import { createEmailService, type EmailConfig, type EmailService } from "@rebasepro/server-core";
37
- import { createHistoryRoutes } from "@rebasepro/server-core";
38
29
  import { HistoryService } from "./history/HistoryService";
39
30
  import { ensureHistoryTableExists } from "./history/ensure-history-table";
40
- import type { Hono } from "hono";
41
- import type { HonoEnv } from "@rebasepro/server-core";
31
+ import { patchPgArrayNullSafety } from "./utils/pg-array-null-patch";
42
32
 
43
33
  export interface PostgresDriverConfig {
44
34
  connectionString?: string;
@@ -108,6 +98,13 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
108
98
  if (pgConfig.schema?.enums) registry.registerEnums(pgConfig.schema.enums as Record<string, PgEnum<[string, ...string[]]>>);
109
99
  if (pgConfig.schema?.relations) registry.registerRelations(pgConfig.schema.relations as Record<string, Relations>);
110
100
 
101
+ // Patch Drizzle's PgArray columns to handle NULL values safely.
102
+ // Drizzle's mapFromDriverValue crashes with "value.map is not a function"
103
+ // when a native array column (text[], integer[], etc.) contains NULL.
104
+ if (pgConfig.schema?.tables) {
105
+ patchPgArrayNullSafety(pgConfig.schema.tables as Record<string, unknown>);
106
+ }
107
+
111
108
  // Build schema-aware Drizzle connection
112
109
  const mergedSchema: Record<string, unknown> = {
113
110
  ...pgConfig.schema?.tables,
@@ -244,8 +244,12 @@ export function serializePropertyToServer(value: unknown, property: Property): u
244
244
  };
245
245
  });
246
246
  }
247
+ return value;
247
248
  }
248
- return value;
249
+ // Non-array value for an array property — coerce to avoid .map() crashes downstream
250
+ logger.warn(`Expected array value for array property, got ${typeof value}. Coercing to empty array.`);
251
+ return [];
252
+
249
253
 
250
254
  case "map":
251
255
  if (typeof value === "object" && property.properties) {
@@ -585,6 +589,10 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
585
589
  };
586
590
  });
587
591
  }
592
+ } else {
593
+ // Non-array value from DB for an array property — coerce to avoid .map() crashes
594
+ logger.warn(`Expected array value from DB for array property, got ${typeof value}. Coercing to array.`);
595
+ return typeof value === "string" ? [value] : [];
588
596
  }
589
597
  return value;
590
598
 
@@ -192,17 +192,17 @@ export class EntityPersistService {
192
192
  }
193
193
  }
194
194
 
195
- // Transform relations to IDs, then sanitize
196
- const serializedResult = serializeDataToServer(otherValues as M, collection.properties as Properties, collection, this.registry);
195
+ let savedId: string | number;
196
+ try {
197
+ // Transform relations to IDs, then sanitize
198
+ const serializedResult = serializeDataToServer(otherValues as M, collection.properties as Properties, collection, this.registry);
197
199
 
198
- // Extract relation updates from the typed result
199
- const inverseRelationUpdates = serializedResult.inverseRelationUpdates;
200
- const joinPathRelationUpdates = serializedResult.joinPathRelationUpdates;
200
+ // Extract relation updates from the typed result
201
+ const inverseRelationUpdates = serializedResult.inverseRelationUpdates;
202
+ const joinPathRelationUpdates = serializedResult.joinPathRelationUpdates;
201
203
 
202
- const entityData = sanitizeAndConvertDates(serializedResult.scalarData);
204
+ const entityData = sanitizeAndConvertDates(serializedResult.scalarData);
203
205
 
204
- let savedId: string | number;
205
- try {
206
206
  savedId = await this.db.transaction(async (tx) => {
207
207
  let currentId: string | number;
208
208
 
@@ -0,0 +1,42 @@
1
+ import { getTableColumns } from "drizzle-orm";
2
+ import { PgArray, PgTable } from "drizzle-orm/pg-core";
3
+ import { logger } from "@rebasepro/server-core";
4
+
5
+ /**
6
+ * Patches all PgArray columns on the given tables to handle NULL values safely.
7
+ *
8
+ * Drizzle ORM's `PgArray.mapFromDriverValue` calls `value.map(...)` without
9
+ * guarding against `null`. When a PostgreSQL native array column (`text[]`,
10
+ * `integer[]`, etc.) contains NULL, the pg driver returns `null` in JavaScript,
11
+ * and `null.map(...)` throws `TypeError: value.map is not a function`.
12
+ *
13
+ * This function walks every column of every registered table and, for any
14
+ * `PgArray` column, wraps its `mapFromDriverValue` to return `null` when the
15
+ * database value is nullish.
16
+ *
17
+ * This is a workaround for a known Drizzle ORM issue. Should be removed once
18
+ * Drizzle handles nullable arrays natively.
19
+ */
20
+ export function patchPgArrayNullSafety(tables: Record<string, unknown>): void {
21
+ let patchedCount = 0;
22
+
23
+ for (const tableOrRelation of Object.values(tables)) {
24
+ if (!(tableOrRelation instanceof PgTable)) continue;
25
+
26
+ const columns = getTableColumns(tableOrRelation);
27
+ for (const column of Object.values(columns)) {
28
+ if (column instanceof PgArray) {
29
+ const original = column.mapFromDriverValue.bind(column);
30
+ column.mapFromDriverValue = function (value: unknown) {
31
+ if (value == null) return null;
32
+ return original(value as string | unknown[]);
33
+ };
34
+ patchedCount++;
35
+ }
36
+ }
37
+ }
38
+
39
+ if (patchedCount > 0) {
40
+ logger.debug(`[PgArray] Patched ${patchedCount} array column(s) for null-safety`);
41
+ }
42
+ }
@@ -0,0 +1,335 @@
1
+ import { describe, it, expect } from "@jest/globals";
2
+ import {
3
+ serializePropertyToServer,
4
+ parsePropertyFromServer,
5
+ serializeDataToServer
6
+ } from "../src/data-transformer";
7
+ import type { Property, Properties, EntityCollection } from "@rebasepro/types";
8
+
9
+ // ─────────────────────────────────────────────────────────────
10
+ // Fixture helpers
11
+ // ─────────────────────────────────────────────────────────────
12
+ function makeCollection(slug: string, properties: Properties): EntityCollection {
13
+ return {
14
+ name: slug,
15
+ slug,
16
+ path: slug,
17
+ collectionType: "postgres",
18
+ tableName: slug,
19
+ properties
20
+ } as unknown as EntityCollection;
21
+ }
22
+
23
+ // ─────────────────────────────────────────────────────────────
24
+ // Property fixtures
25
+ // ─────────────────────────────────────────────────────────────
26
+ const arrayOfStringsProp: Property = {
27
+ type: "array",
28
+ name: "Tags",
29
+ of: { type: "string", name: "Tag" }
30
+ } as Property;
31
+
32
+ const arrayOfNumbersProp: Property = {
33
+ type: "array",
34
+ name: "Scores",
35
+ of: { type: "number", name: "Score" }
36
+ } as Property;
37
+
38
+ const arrayOfMapsProp: Property = {
39
+ type: "array",
40
+ name: "Addresses",
41
+ of: {
42
+ type: "map",
43
+ name: "Address",
44
+ properties: {
45
+ street: { type: "string", name: "Street" },
46
+ city: { type: "string", name: "City" }
47
+ }
48
+ }
49
+ } as Property;
50
+
51
+ const arrayOneOfProp: Property = {
52
+ type: "array",
53
+ name: "Blocks",
54
+ oneOf: {
55
+ typeField: "type",
56
+ valueField: "value",
57
+ properties: {
58
+ text: { type: "string", name: "Text" } as Property,
59
+ number: { type: "number", name: "Number" } as Property
60
+ }
61
+ }
62
+ } as Property;
63
+
64
+ const arrayOfRelationsProp: Property = {
65
+ type: "array",
66
+ name: "Authors",
67
+ of: { type: "relation", name: "Author" }
68
+ } as Property;
69
+
70
+ // A bare array property without `of` or `oneOf`
71
+ const bareArrayProp: Property = {
72
+ type: "array",
73
+ name: "RawArray"
74
+ } as Property;
75
+
76
+ // ─────────────────────────────────────────────────────────────
77
+ // 1. serializePropertyToServer — array null safety
78
+ // ─────────────────────────────────────────────────────────────
79
+ describe("serializePropertyToServer — array null safety", () => {
80
+
81
+ // ── Null / undefined early return ──
82
+ it("returns null when value is null with array-of-strings property", () => {
83
+ expect(serializePropertyToServer(null, arrayOfStringsProp)).toBeNull();
84
+ });
85
+
86
+ it("returns undefined when value is undefined with array-of-strings property", () => {
87
+ expect(serializePropertyToServer(undefined, arrayOfStringsProp)).toBeUndefined();
88
+ });
89
+
90
+ // ── Empty array ──
91
+ it("returns [] when value is an empty array", () => {
92
+ expect(serializePropertyToServer([], arrayOfStringsProp)).toEqual([]);
93
+ });
94
+
95
+ // ── Null with different sub-types ──
96
+ it("returns null when value is null with array-of-numbers property", () => {
97
+ expect(serializePropertyToServer(null, arrayOfNumbersProp)).toBeNull();
98
+ });
99
+
100
+ it("returns null when value is null with array-of-maps property", () => {
101
+ expect(serializePropertyToServer(null, arrayOfMapsProp)).toBeNull();
102
+ });
103
+
104
+ it("returns null when value is null with array oneOf property", () => {
105
+ expect(serializePropertyToServer(null, arrayOneOfProp)).toBeNull();
106
+ });
107
+
108
+ // ── Non-array values (coerced to empty array) ──
109
+ it("coerces a non-array string to empty array for array property", () => {
110
+ expect(serializePropertyToServer("not-an-array", arrayOfStringsProp)).toEqual([]);
111
+ });
112
+
113
+ it("coerces a non-array number to empty array for array property", () => {
114
+ expect(serializePropertyToServer(42, arrayOfStringsProp)).toEqual([]);
115
+ });
116
+
117
+ it("coerces a non-array object to empty array for bare array property", () => {
118
+ const obj = { a: 1 };
119
+ expect(serializePropertyToServer(obj, bareArrayProp)).toEqual([]);
120
+ });
121
+
122
+ it("coerces a boolean to empty array for array property", () => {
123
+ expect(serializePropertyToServer(true, arrayOfStringsProp)).toEqual([]);
124
+ expect(serializePropertyToServer(false, arrayOfStringsProp)).toEqual([]);
125
+ });
126
+
127
+ // ── Arrays with null / undefined elements ──
128
+ it("handles array with null elements by passing each through sub-serialization", () => {
129
+ const result = serializePropertyToServer([null, "a", null], arrayOfStringsProp);
130
+ expect(result).toEqual([null, "a", null]);
131
+ });
132
+
133
+ it("handles array with undefined elements by passing each through sub-serialization", () => {
134
+ const result = serializePropertyToServer([undefined, "a"], arrayOfStringsProp);
135
+ expect(result).toEqual([undefined, "a"]);
136
+ });
137
+
138
+ // ── Array of relation objects with null entries ──
139
+ it("serializes array of relation objects with null entries correctly", () => {
140
+ const value = [{ id: "1" }, null, { id: "2" }];
141
+ const result = serializePropertyToServer(value, arrayOfRelationsProp);
142
+ // null entries pass through serializePropertyToServer for the sub-property
143
+ // which returns null for null values
144
+ expect(result).toEqual(["1", null, "2"]);
145
+ });
146
+
147
+ // ── Array of maps where some entries are null ──
148
+ it("handles array of maps where some entries are null gracefully", () => {
149
+ const value = [
150
+ { street: "123 Main", city: "NYC" },
151
+ null,
152
+ { street: "456 Oak", city: "LA" }
153
+ ];
154
+ const result = serializePropertyToServer(value, arrayOfMapsProp);
155
+ expect(result).toEqual([
156
+ { street: "123 Main", city: "NYC" },
157
+ null,
158
+ { street: "456 Oak", city: "LA" }
159
+ ]);
160
+ });
161
+
162
+ // ── Empty array with oneOf property ──
163
+ it("returns [] when value is an empty array with oneOf property", () => {
164
+ expect(serializePropertyToServer([], arrayOneOfProp)).toEqual([]);
165
+ });
166
+
167
+ // ── Nested array of arrays (unusual) ──
168
+ it("returns nested array of arrays as-is for bare array property", () => {
169
+ const nested = [["a", "b"], ["c"]];
170
+ // Without `of` or `oneOf`, the array case falls through to `return value`
171
+ expect(serializePropertyToServer(nested, bareArrayProp)).toEqual([["a", "b"], ["c"]]);
172
+ });
173
+ });
174
+
175
+ // ─────────────────────────────────────────────────────────────
176
+ // 2. parsePropertyFromServer — array null safety
177
+ // ─────────────────────────────────────────────────────────────
178
+ describe("parsePropertyFromServer — array null safety", () => {
179
+ const dummyCollection = makeCollection("items", {
180
+ tags: arrayOfStringsProp
181
+ });
182
+
183
+ // ── Null / undefined early return ──
184
+ it("returns null when value is null with array property", () => {
185
+ expect(parsePropertyFromServer(null, arrayOfStringsProp, dummyCollection)).toBeNull();
186
+ });
187
+
188
+ it("returns undefined when value is undefined with array property", () => {
189
+ expect(parsePropertyFromServer(undefined, arrayOfStringsProp, dummyCollection)).toBeUndefined();
190
+ });
191
+
192
+ // ── Empty array ──
193
+ it("returns [] when value is an empty array with array-of-strings", () => {
194
+ expect(parsePropertyFromServer([], arrayOfStringsProp, dummyCollection)).toEqual([]);
195
+ });
196
+
197
+ // ── Non-array values (coerced to array) ──
198
+ it("coerces a non-array string to single-element array for array property", () => {
199
+ expect(parsePropertyFromServer("not-an-array", arrayOfStringsProp, dummyCollection)).toEqual(["not-an-array"]);
200
+ });
201
+
202
+ it("coerces a non-array number to empty array for array property", () => {
203
+ expect(parsePropertyFromServer(99, arrayOfStringsProp, dummyCollection)).toEqual([]);
204
+ });
205
+
206
+ // ── Null with oneOf ──
207
+ it("returns null when value is null with array oneOf property", () => {
208
+ expect(parsePropertyFromServer(null, arrayOneOfProp, dummyCollection)).toBeNull();
209
+ });
210
+
211
+ // ── Array with null elements ──
212
+ it("handles array with null elements for parsing gracefully", () => {
213
+ const result = parsePropertyFromServer([null, "a", null], arrayOfStringsProp, dummyCollection);
214
+ expect(result).toEqual([null, "a", null]);
215
+ });
216
+
217
+ // ── Falsy non-null values (coerced to array) ──
218
+ it("coerces falsy number 0 to empty array for array property", () => {
219
+ expect(parsePropertyFromServer(0, arrayOfStringsProp, dummyCollection)).toEqual([]);
220
+ });
221
+
222
+ it("coerces false to empty array for array property", () => {
223
+ expect(parsePropertyFromServer(false, arrayOfStringsProp, dummyCollection)).toEqual([]);
224
+ });
225
+
226
+ it("coerces empty string to single-element array for array property", () => {
227
+ expect(parsePropertyFromServer("", arrayOfStringsProp, dummyCollection)).toEqual([""]);
228
+ });
229
+ });
230
+
231
+ // ─────────────────────────────────────────────────────────────
232
+ // 3. serializeDataToServer — entity with null array fields
233
+ // ─────────────────────────────────────────────────────────────
234
+ describe("serializeDataToServer — entity with null array fields", () => {
235
+ const tagsProperty: Property = {
236
+ type: "array",
237
+ name: "Tags",
238
+ of: { type: "string", name: "Tag" }
239
+ } as Property;
240
+
241
+ const categoriesProperty: Property = {
242
+ type: "array",
243
+ name: "Categories",
244
+ of: { type: "string", name: "Category" }
245
+ } as Property;
246
+
247
+ const metadataProperty: Property = {
248
+ type: "map",
249
+ name: "Metadata",
250
+ properties: {
251
+ labels: {
252
+ type: "array",
253
+ name: "Labels",
254
+ of: { type: "string", name: "Label" }
255
+ } as Property
256
+ }
257
+ } as Property;
258
+
259
+ const properties: Properties = {
260
+ tags: tagsProperty
261
+ };
262
+
263
+ // ── Null array field ──
264
+ it("serializes entity with tags: null — scalarData.tags should be null", () => {
265
+ const result = serializeDataToServer(
266
+ { tags: null },
267
+ properties
268
+ );
269
+ expect(result.scalarData.tags).toBeNull();
270
+ });
271
+
272
+ // ── Undefined array field ──
273
+ it("serializes entity with tags: undefined — tags value should be undefined in scalarData", () => {
274
+ const result = serializeDataToServer(
275
+ { tags: undefined },
276
+ properties
277
+ );
278
+ // Object.entries iterates keys even when value is undefined,
279
+ // and serializePropertyToServer returns undefined for undefined input
280
+ expect(result.scalarData.tags).toBeUndefined();
281
+ });
282
+
283
+ // ── Empty array field ──
284
+ it("serializes entity with tags: [] — scalarData.tags should be []", () => {
285
+ const result = serializeDataToServer(
286
+ { tags: [] },
287
+ properties
288
+ );
289
+ expect(result.scalarData.tags).toEqual([]);
290
+ });
291
+
292
+ // ── Multiple array fields, mixed null / populated ──
293
+ it("handles entity with multiple array fields, some null, some populated", () => {
294
+ const multiProperties: Properties = {
295
+ tags: tagsProperty,
296
+ categories: categoriesProperty
297
+ };
298
+
299
+ const result = serializeDataToServer(
300
+ { tags: null, categories: ["cat-a", "cat-b"] },
301
+ multiProperties
302
+ );
303
+ expect(result.scalarData.tags).toBeNull();
304
+ expect(result.scalarData.categories).toEqual(["cat-a", "cat-b"]);
305
+ });
306
+
307
+ // ── Nested map containing null array ──
308
+ it("handles entity with nested map containing array field that is null", () => {
309
+ const nestedProperties: Properties = {
310
+ metadata: metadataProperty
311
+ };
312
+
313
+ const result = serializeDataToServer(
314
+ { metadata: { labels: null } },
315
+ nestedProperties
316
+ );
317
+ // The map serializer recurses into sub-properties; the inner array gets null-checked
318
+ expect(result.scalarData.metadata).toEqual({ labels: null });
319
+ });
320
+
321
+ // ── All array fields null ──
322
+ it("handles entity with only array fields, all null — scalarData should have all null values", () => {
323
+ const allArrayProperties: Properties = {
324
+ tags: tagsProperty,
325
+ categories: categoriesProperty
326
+ };
327
+
328
+ const result = serializeDataToServer(
329
+ { tags: null, categories: null },
330
+ allArrayProperties
331
+ );
332
+ expect(result.scalarData.tags).toBeNull();
333
+ expect(result.scalarData.categories).toBeNull();
334
+ });
335
+ });
@@ -149,14 +149,65 @@ path: "authors" }
149
149
  of: { type: "string" } as Property
150
150
  } as unknown as Property;
151
151
 
152
+ const arrayOfNumbersProp: Property = {
153
+ type: "array",
154
+ of: { type: "number" } as Property
155
+ } as unknown as Property;
156
+
157
+ const arrayOfRelationsProp: Property = {
158
+ type: "array",
159
+ of: { type: "relation" } as Property
160
+ } as unknown as Property;
161
+
152
162
  it("should serialize array elements through their sub-property type", () => {
153
163
  expect(serializePropertyToServer(["a", "b", "c"], arrayOfStringsProp))
154
164
  .toEqual(["a", "b", "c"]);
155
165
  });
156
166
 
157
- it("should pass through non-array values", () => {
167
+ it("should coerce non-array values to empty array", () => {
158
168
  expect(serializePropertyToServer("not-an-array", arrayOfStringsProp))
159
- .toBe("not-an-array");
169
+ .toEqual([]);
170
+ });
171
+
172
+ // --- Null safety ---
173
+ it("should return null for null array value", () => {
174
+ expect(serializePropertyToServer(null, arrayOfStringsProp)).toBeNull();
175
+ });
176
+
177
+ it("should return undefined for undefined array value", () => {
178
+ expect(serializePropertyToServer(undefined, arrayOfStringsProp)).toBeUndefined();
179
+ });
180
+
181
+ it("should return empty array for empty array value", () => {
182
+ expect(serializePropertyToServer([], arrayOfStringsProp)).toEqual([]);
183
+ });
184
+
185
+ it("should return null for null value with array-of-numbers property", () => {
186
+ expect(serializePropertyToServer(null, arrayOfNumbersProp)).toBeNull();
187
+ });
188
+
189
+ it("should handle array with null elements", () => {
190
+ const result = serializePropertyToServer([null, "a", null, "b"], arrayOfStringsProp);
191
+ expect(result).toEqual([null, "a", null, "b"]);
192
+ });
193
+
194
+ it("should handle array of relation objects with null entries", () => {
195
+ const value = [{ id: "1", path: "tags" }, null, { id: "2", path: "tags" }];
196
+ const result = serializePropertyToServer(value, arrayOfRelationsProp);
197
+ expect(result).toEqual(["1", null, "2"]);
198
+ });
199
+
200
+ it("should coerce number value to empty array for array property", () => {
201
+ expect(serializePropertyToServer(42, arrayOfStringsProp)).toEqual([]);
202
+ });
203
+
204
+ it("should coerce boolean value to empty array for array property", () => {
205
+ expect(serializePropertyToServer(true, arrayOfStringsProp)).toEqual([]);
206
+ });
207
+
208
+ it("should coerce object value to empty array for array property", () => {
209
+ const obj = { a: 1 };
210
+ expect(serializePropertyToServer(obj, arrayOfStringsProp)).toEqual([]);
160
211
  });
161
212
  });
162
213
 
@@ -0,0 +1,65 @@
1
+ import { describe, it, expect, jest } from "@jest/globals";
2
+ import { pgTable, text, integer, boolean } from "drizzle-orm/pg-core";
3
+ import { patchPgArrayNullSafety } from "../src/utils/pg-array-null-patch";
4
+ import { getTableColumns } from "drizzle-orm";
5
+ import { PgArray } from "drizzle-orm/pg-core";
6
+
7
+ // Build a table that has native array columns
8
+ const testTable = pgTable("test_items", {
9
+ id: text("id").primaryKey(),
10
+ name: text("name"),
11
+ tags: text("tags").array(),
12
+ scores: integer("scores").array(),
13
+ flags: boolean("flags").array(),
14
+ });
15
+
16
+ describe("patchPgArrayNullSafety", () => {
17
+ it("should patch PgArray columns to handle null", () => {
18
+ patchPgArrayNullSafety({ test_items: testTable });
19
+
20
+ const columns = getTableColumns(testTable);
21
+ const tagsColumn = columns.tags;
22
+
23
+ // Before patch, calling mapFromDriverValue(null) would throw TypeError
24
+ // After patch, it should return null
25
+ expect(tagsColumn.mapFromDriverValue(null as any)).toBeNull();
26
+ expect(tagsColumn.mapFromDriverValue(undefined as any)).toBeNull();
27
+ });
28
+
29
+ it("should still process valid array values correctly", () => {
30
+ patchPgArrayNullSafety({ test_items: testTable });
31
+
32
+ const columns = getTableColumns(testTable);
33
+ const tagsColumn = columns.tags;
34
+ const scoresColumn = columns.scores;
35
+
36
+ // Valid arrays should still work
37
+ expect(tagsColumn.mapFromDriverValue(["hello", "world"])).toEqual(["hello", "world"]);
38
+ expect(tagsColumn.mapFromDriverValue([])).toEqual([]);
39
+ expect(scoresColumn.mapFromDriverValue([1, 2, 3])).toEqual([1, 2, 3]);
40
+ });
41
+
42
+ it("should not patch non-array columns", () => {
43
+ patchPgArrayNullSafety({ test_items: testTable });
44
+
45
+ const columns = getTableColumns(testTable);
46
+ const nameColumn = columns.name;
47
+
48
+ // Non-array columns should not be PgArray instances
49
+ expect(nameColumn instanceof PgArray).toBe(false);
50
+ });
51
+
52
+ it("should handle tables with no array columns", () => {
53
+ const simpleTable = pgTable("simple", {
54
+ id: text("id").primaryKey(),
55
+ name: text("name"),
56
+ });
57
+
58
+ // Should not throw
59
+ expect(() => patchPgArrayNullSafety({ simple: simpleTable })).not.toThrow();
60
+ });
61
+
62
+ it("should handle empty input", () => {
63
+ expect(() => patchPgArrayNullSafety({})).not.toThrow();
64
+ });
65
+ });