firestore-proto-codec 0.2.0 → 0.3.0

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/README.md CHANGED
@@ -45,34 +45,35 @@ int64; decoding hands them back as strings.
45
45
  ## Binding to an SDK
46
46
 
47
47
  The default adapter emits dependency-free `FsTimestamp` / `FsBlob` /
48
- `FsGeoPoint`. Supply your own to write straight to Firestore:
48
+ `FsGeoPoint`, which the admin SDK refuses outright: it rejects custom
49
+ prototypes, so a forgotten adapter throws at the write and stores nothing.
50
+
51
+ For `firebase-admin` / `@google-cloud/firestore`, the adapter ships:
49
52
 
50
53
  ```ts
51
- class AdminTypes implements FirestoreTypes {
52
- timestamp = (seconds: bigint, nanos: number) =>
53
- new Timestamp(Number(seconds), nanos);
54
- blob = (bytes: Uint8Array) => Buffer.from(bytes);
55
- geoPoint = (lat: number, lng: number) => new GeoPoint(lat, lng);
56
-
57
- readTimestamp = (v: unknown) =>
58
- v instanceof Timestamp
59
- ? { seconds: BigInt(v.seconds), nanos: v.nanoseconds }
60
- : undefined;
61
- // Buffer extends Uint8Array, so this covers both.
62
- readBlob = (v: unknown) =>
63
- v instanceof Uint8Array ? new Uint8Array(v) : undefined;
64
- readGeoPoint = (v: unknown) =>
65
- v instanceof GeoPoint
66
- ? { latitude: v.latitude, longitude: v.longitude }
67
- : undefined;
68
- }
69
-
70
- const codec = new FirestoreProtoCodec(new AdminTypes());
54
+ import { adminCodec } from "firestore-proto-codec/admin";
55
+
56
+ await doc.set(adminCodec.encode(TaskSchema, task));
71
57
  ```
72
58
 
73
- Verified against `@google-cloud/firestore` 9.0.1: there is no `Bytes` class in
74
- the admin SDK (that one belongs to the web client SDK), and bytes values are
75
- plain `Buffer`s.
59
+ `AdminFirestoreTypes` comes from the same subpath if you would rather construct
60
+ the codec yourself. `@google-cloud/firestore` is an **optional peer
61
+ dependency** — only that subpath imports it, so the root entry point still
62
+ carries no Firebase dependency. `firebase-admin` already depends on it, so a
63
+ hoisted layout resolves it with nothing added; a strict one (pnpm, Yarn PnP)
64
+ needs `@google-cloud/firestore` in your own dependencies.
65
+
66
+ Its reads are structural rather than `instanceof`. The codec only calls them at
67
+ a position the schema has already declared a `Timestamp`, `bytes` or `LatLng`,
68
+ so there is no type to discriminate and nothing to lose — and a structural
69
+ check survives two copies of `@google-cloud/firestore` in the tree, which would
70
+ otherwise make `instanceof` false and fail every read with
71
+ `expected a timestamp`.
72
+
73
+ For any other SDK, implement `FirestoreTypes` yourself; [src/admin.ts](src/admin.ts)
74
+ is the worked example. Verified against `@google-cloud/firestore` 9.0.1: there
75
+ is no `Bytes` class in the admin SDK (that one belongs to the web client SDK),
76
+ and bytes values are plain `Buffer`s.
76
77
 
77
78
  Keeping this an interface is why the core has no Firebase dependency.
78
79
 
@@ -164,8 +165,9 @@ It needs a JDK 21 or newer, which the Firestore emulator requires;
164
165
  `firebase-tools` is fetched by the script at a pinned version, so the emulator
165
166
  build is the same one CI runs. The suite is skipped when
166
167
  `FIRESTORE_EMULATOR_HOST` is unset, so `npm test` stays green without a JDK.
167
- `@google-cloud/firestore` is a dev dependency only -- the published package
168
- still has no Firebase dependency.
168
+ `@google-cloud/firestore` is a dev dependency and an optional peer -- nothing
169
+ but the `/admin` subpath imports it, so the root entry point has no Firebase
170
+ dependency.
169
171
 
170
172
  Nothing in a pull request can change what the server does, so CI runs this
171
173
  [weekly](../.github/workflows/emulator.yml) rather than per-PR, and it can be
@@ -0,0 +1,28 @@
1
+ import { FirestoreProtoCodec } from "./codec.js";
2
+ import type { FirestoreTypes } from "./values.js";
3
+ /**
4
+ * The reads are structural rather than `instanceof`. They can afford to be:
5
+ * the codec never uses them to discriminate a type, only to unwrap a value at
6
+ * a position the schema has already declared a `Timestamp`, `bytes` or
7
+ * `LatLng`, so a loose check cannot misclassify anything. What it buys is
8
+ * immunity to two copies of `@google-cloud/firestore` in the tree -- a
9
+ * version-range accident a consumer does not control, which makes `instanceof`
10
+ * false and turns every read into `expected a timestamp`. That failure reads
11
+ * like data corruption rather than like a dependency problem.
12
+ */
13
+ export declare class AdminFirestoreTypes implements FirestoreTypes {
14
+ timestamp(seconds: bigint, nanos: number): unknown;
15
+ blob(bytes: Uint8Array): unknown;
16
+ geoPoint(latitude: number, longitude: number): unknown;
17
+ readTimestamp(value: unknown): {
18
+ seconds: bigint;
19
+ nanos: number;
20
+ } | undefined;
21
+ readBlob(value: unknown): Uint8Array | undefined;
22
+ readGeoPoint(value: unknown): {
23
+ latitude: number;
24
+ longitude: number;
25
+ } | undefined;
26
+ }
27
+ /** Shared instance: the codec keeps no per-call state. */
28
+ export declare const adminCodec: FirestoreProtoCodec;
package/dist/admin.js ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `FirestoreTypes` for the Firestore Admin SDK. `firebase-admin/firestore` and
3
+ * `@google-cloud/firestore` re-export the same `Timestamp` and `GeoPoint`, so
4
+ * this adapter serves both.
5
+ *
6
+ * ```ts
7
+ * import { adminCodec } from "firestore-proto-codec/admin";
8
+ * ```
9
+ *
10
+ * Not a convenience: without an adapter the codec emits `FsTimestamp` /
11
+ * `FsBlob` / `FsGeoPoint`, and the admin SDK refuses a custom prototype
12
+ * outright, so the write throws and stores nothing (pinned by
13
+ * test/emulator.test.ts). Every admin consumer needs this, which is why it
14
+ * ships here rather than being copied out of the README.
15
+ *
16
+ * `@google-cloud/firestore` is an optional peer dependency. Only this module
17
+ * imports it -- the root entry point stays dependency-free.
18
+ */
19
+ import { GeoPoint, Timestamp } from "@google-cloud/firestore";
20
+ import { FirestoreProtoCodec } from "./codec.js";
21
+ /**
22
+ * The reads are structural rather than `instanceof`. They can afford to be:
23
+ * the codec never uses them to discriminate a type, only to unwrap a value at
24
+ * a position the schema has already declared a `Timestamp`, `bytes` or
25
+ * `LatLng`, so a loose check cannot misclassify anything. What it buys is
26
+ * immunity to two copies of `@google-cloud/firestore` in the tree -- a
27
+ * version-range accident a consumer does not control, which makes `instanceof`
28
+ * false and turns every read into `expected a timestamp`. That failure reads
29
+ * like data corruption rather than like a dependency problem.
30
+ */
31
+ export class AdminFirestoreTypes {
32
+ timestamp(seconds, nanos) {
33
+ // Firestore spans years 1--9999, so seconds stays far inside
34
+ // Number.MAX_SAFE_INTEGER and the narrowing is lossless.
35
+ return new Timestamp(Number(seconds), nanos);
36
+ }
37
+ blob(bytes) {
38
+ // Copies rather than viewing `bytes.buffer`: a pending write must not
39
+ // change under a caller who mutates the message after encoding it.
40
+ return Buffer.from(bytes);
41
+ }
42
+ geoPoint(latitude, longitude) {
43
+ return new GeoPoint(latitude, longitude);
44
+ }
45
+ readTimestamp(value) {
46
+ const ts = value;
47
+ return typeof ts?.seconds === "number" &&
48
+ typeof ts?.nanoseconds === "number" &&
49
+ typeof ts?.toDate === "function"
50
+ ? { seconds: BigInt(ts.seconds), nanos: ts.nanoseconds }
51
+ : undefined;
52
+ }
53
+ readBlob(value) {
54
+ // `instanceof` is right here, unlike above: `Uint8Array` is a realm
55
+ // intrinsic, not a package export, so a duplicated dependency cannot
56
+ // detach it. The copy hands the decoded message a plain `Uint8Array`
57
+ // rather than the SDK's `Buffer`, which a strict deep-equal against an
58
+ // encoded message would otherwise report as a type mismatch.
59
+ return value instanceof Uint8Array ? new Uint8Array(value) : undefined;
60
+ }
61
+ readGeoPoint(value) {
62
+ const point = value;
63
+ return typeof point?.latitude === "number" &&
64
+ typeof point?.longitude === "number"
65
+ ? { latitude: point.latitude, longitude: point.longitude }
66
+ : undefined;
67
+ }
68
+ }
69
+ /** Shared instance: the codec keeps no per-call state. */
70
+ export const adminCodec = new FirestoreProtoCodec(new AdminFirestoreTypes());
package/dist/values.d.ts CHANGED
@@ -1,8 +1,13 @@
1
1
  /**
2
2
  * Firestore has three value types protobuf cannot express as plain data.
3
3
  * Implement this to bind them to a particular SDK's classes -- `Timestamp`,
4
- * `Bytes`, and `GeoPoint` in `firebase-admin`, for example. Everything else in
5
- * the encoding is a plain string, number, bigint, boolean, array, or object.
4
+ * `Buffer`, and `GeoPoint` in `firebase-admin`, for example. (Bytes are plain
5
+ * `Buffer`s there; the `Bytes` class belongs to the web client SDK.)
6
+ * Everything else in the encoding is a plain string, number, bigint, boolean,
7
+ * array, or object.
8
+ *
9
+ * The admin implementation ships as `firestore-proto-codec/admin`. Write your
10
+ * own only for another SDK.
6
11
  */
7
12
  export interface FirestoreTypes {
8
13
  timestamp(seconds: bigint, nanos: number): unknown;
@@ -34,8 +39,9 @@ export declare class FsGeoPoint {
34
39
  readonly longitude: number;
35
40
  constructor(latitude: number, longitude: number);
36
41
  }
37
- /** Dependency-free default. Swap in an SDK-specific implementation to write
38
- * directly to Firestore. */
42
+ /** Dependency-free default. It cannot write to Firestore: the admin SDK
43
+ * refuses these custom prototypes outright. Swap in an SDK-specific
44
+ * implementation -- `firestore-proto-codec/admin` for firebase-admin. */
39
45
  export declare class DefaultFirestoreTypes implements FirestoreTypes {
40
46
  timestamp(seconds: bigint, nanos: number): unknown;
41
47
  blob(bytes: Uint8Array): unknown;
package/dist/values.js CHANGED
@@ -20,8 +20,9 @@ export class FsGeoPoint {
20
20
  this.longitude = longitude;
21
21
  }
22
22
  }
23
- /** Dependency-free default. Swap in an SDK-specific implementation to write
24
- * directly to Firestore. */
23
+ /** Dependency-free default. It cannot write to Firestore: the admin SDK
24
+ * refuses these custom prototypes outright. Swap in an SDK-specific
25
+ * implementation -- `firestore-proto-codec/admin` for firebase-admin. */
25
26
  export class DefaultFirestoreTypes {
26
27
  timestamp(seconds, nanos) {
27
28
  return new FsTimestamp(seconds, nanos);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firestore-proto-codec",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Encode a protobuf message as a Firestore value, and decode it back.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -16,6 +16,10 @@
16
16
  "types": "./dist/index.d.ts",
17
17
  "default": "./dist/index.js"
18
18
  },
19
+ "./admin": {
20
+ "types": "./dist/admin.d.ts",
21
+ "default": "./dist/admin.js"
22
+ },
19
23
  "./proto/*": "./proto/*"
20
24
  },
21
25
  "files": [
@@ -34,6 +38,14 @@
34
38
  "dependencies": {
35
39
  "@bufbuild/protobuf": "^2.2.0"
36
40
  },
41
+ "peerDependencies": {
42
+ "@google-cloud/firestore": ">=6"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "@google-cloud/firestore": {
46
+ "optional": true
47
+ }
48
+ },
37
49
  "devDependencies": {
38
50
  "@bufbuild/protoc-gen-es": "^2.2.0",
39
51
  "@google-cloud/firestore": "^9.0.1",