1import * as GCP from "@/GCP";
2import {
3 decodeFields,
4 encodeFields,
5 encodeValue,
6 fieldPath,
7} from "@/GCP/Firestore/Values.ts";
8import * as Test from "@/Test/Alchemy";
9import { describe, expect } from "alchemy-test";
10import * as Effect from "effect/Effect";
11
12const { test } = Test.make({ providers: GCP.providers() });
13
14const when = new Date("2026-01-02T03:04:05.678Z");
15const sample = {
16 str: "hello",
17 int: 42,
18 negative: -7,
19 double: 3.5,
20 big: 9007199254740993n,
21 yes: true,
22 no: false,
23 nothing: null,
24 when,
25 bytes: new Uint8Array([0, 1, 254, 255]),
26 list: [1, "two", false, null, { x: 3 }],
27 nested: { a: { b: "deep" }, n: 1.25 },
28};
29
30/** Comparable view of decoded fields (bigint, Date and bytes flattened). */
31const describeFields = (fields: Record<string, unknown>) => ({
32 str: fields.str,
33 int: fields.int,
34 negative: fields.negative,
35 double: fields.double,
36 big: typeof fields.big === "bigint" ? fields.big.toString() : fields.big,
37 yes: fields.yes,
38 no: fields.no,
39 nothing: fields.nothing,
40 when: fields.when instanceof Date ? fields.when.toISOString() : fields.when,
41 bytes: fields.bytes instanceof Uint8Array ? [...fields.bytes] : fields.bytes,
42 list: fields.list,
43 nested: fields.nested,
44});
45
46const expectedFields = {
47 str: "hello",
48 int: 42,
49 negative: -7,
50 double: 3.5,
51 big: "9007199254740993",
52 yes: true,
53 no: false,
54 nothing: null,
55 when: "2026-01-02T03:04:05.678Z",
56 bytes: [0, 1, 254, 255],
57 list: [1, "two", false, null, { x: 3 }],
58 nested: { a: { b: "deep" }, n: 1.25 },
59};
60
61describe("Values codec", () => {
62 test(
63 "encodes plain JavaScript to Firestore Values",
64 Effect.sync(() => {
65 expect(encodeFields(sample)).toEqual({
66 str: { stringValue: "hello" },
67 int: { integerValue: "42" },
68 negative: { integerValue: "-7" },
69 double: { doubleValue: 3.5 },
70 big: { integerValue: "9007199254740993" },
71 yes: { booleanValue: true },
72 no: { booleanValue: false },
73 nothing: { nullValue: "NULL_VALUE" },
74 when: { timestampValue: "2026-01-02T03:04:05.678Z" },
75 bytes: { bytesValue: "AAH+/w==" },
76 list: {
77 arrayValue: {
78 values: [
79 { integerValue: "1" },
80 { stringValue: "two" },
81 { booleanValue: false },
82 { nullValue: "NULL_VALUE" },
83 { mapValue: { fields: { x: { integerValue: "3" } } } },
84 ],
85 },
86 },
87 nested: {
88 mapValue: {
89 fields: {
90 a: { mapValue: { fields: { b: { stringValue: "deep" } } } },
91 n: { doubleValue: 1.25 },
92 },
93 },
94 },
95 });
96 expect(encodeFields({ skipped: undefined })).toEqual({});
97 expect(encodeValue(undefined)).toEqual({ nullValue: "NULL_VALUE" });
98 }),
99 { tags: ["unit", "provider:gcp", "provider:gcp:firestore", "local"] },
100 );
101
102 test(
103 "decodes Firestore Values to plain JavaScript",
104 Effect.sync(() => {
105 expect(describeFields(decodeFields(encodeFields(sample)))).toEqual(
106 expectedFields,
107 );
108 expect(
109 decodeFields({
110 geo: { geoPointValue: { latitude: 1.5, longitude: -2 } },
111 ref: { referenceValue: "projects/p/databases/d/documents/a/b" },
112 emptyList: { arrayValue: {} },
113 emptyMap: { mapValue: {} },
114 }),
115 ).toEqual({
116 geo: { latitude: 1.5, longitude: -2 },
117 ref: "projects/p/databases/d/documents/a/b",
118 emptyList: [],
119 emptyMap: {},
120 });
121 }),
122 { tags: ["unit", "provider:gcp", "provider:gcp:firestore", "local"] },
123 );
124
125 test(
126 "quotes non-identifier update-mask field paths",
127 Effect.sync(() => {
128 expect(fieldPath("plain_Name1")).toEqual("plain_Name1");
129 expect(fieldPath("has space")).toEqual("`has space`");
130 expect(fieldPath("a.b")).toEqual("`a.b`");
131 expect(fieldPath("tick`")).toEqual("`tick\\``");
132 }),
133 { tags: ["unit", "provider:gcp", "provider:gcp:firestore", "local"] },
134 );
135});