firestore-proto-codec 0.1.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 +28 -26
- package/dist/admin.d.ts +28 -0
- package/dist/admin.js +70 -0
- package/dist/generated/codebinge/firestore/codec/v1/options_pb.d.ts +1 -1
- package/dist/generated/codebinge/firestore/codec/v1/options_pb.js +2 -2
- package/dist/values.d.ts +10 -4
- package/dist/values.js +3 -2
- package/package.json +13 -1
- package/proto/codebinge/firestore/codec/v1/options.proto +10 -8
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
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
74
|
-
the
|
|
75
|
-
|
|
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
|
|
168
|
-
|
|
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
|
package/dist/admin.d.ts
ADDED
|
@@ -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());
|
|
@@ -100,6 +100,6 @@ export declare enum EnumEncoding {
|
|
|
100
100
|
*/
|
|
101
101
|
export declare const EnumEncodingSchema: GenEnum<EnumEncoding>;
|
|
102
102
|
/**
|
|
103
|
-
* @generated from extension: codebinge.firestore.codec.v1.Field field =
|
|
103
|
+
* @generated from extension: codebinge.firestore.codec.v1.Field field = 1376;
|
|
104
104
|
*/
|
|
105
105
|
export declare const field: GenExtension<FieldOptions, Field>;
|
|
@@ -4,7 +4,7 @@ import { file_google_protobuf_descriptor } from "@bufbuild/protobuf/wkt";
|
|
|
4
4
|
/**
|
|
5
5
|
* Describes the file codebinge/firestore/codec/v1/options.proto.
|
|
6
6
|
*/
|
|
7
|
-
export const file_codebinge_firestore_codec_v1_options = /*@__PURE__*/ fileDesc("
|
|
7
|
+
export const file_codebinge_firestore_codec_v1_options = /*@__PURE__*/ fileDesc("Cipjb2RlYmluZ2UvZmlyZXN0b3JlL2NvZGVjL3YxL29wdGlvbnMucHJvdG8SHGNvZGViaW5nZS5maXJlc3RvcmUuY29kZWMudjEiyAEKBUZpZWxkEgwKBHNraXAYASABKAgSDAoEbmFtZRgCIAEoCRI7CgdlbnVtX2FzGAMgASgOMiouY29kZWJpbmdlLmZpcmVzdG9yZS5jb2RlYy52MS5FbnVtRW5jb2RpbmcSHgoRb21pdF93aGVuX2RlZmF1bHQYBCABKAhIAIgBARIwCgRraW5kGAUgASgOMiIuY29kZWJpbmdlLmZpcmVzdG9yZS5jb2RlYy52MS5LaW5kQhQKEl9vbWl0X3doZW5fZGVmYXVsdCpOCgRLaW5kEhQKEEtJTkRfVU5TUEVDSUZJRUQQABISCg5LSU5EX0dFT19QT0lOVBABEhwKGEtJTkRfVU5TSUdORURfQVNfSU5URUdFUhACKkAKDEVudW1FbmNvZGluZxIWChJFTlVNX0VOQ09ESU5HX05BTUUQABIYChRFTlVNX0VOQ09ESU5HX05VTUJFUhABOlkKBWZpZWxkEh0uZ29vZ2xlLnByb3RvYnVmLkZpZWxkT3B0aW9ucxjgCiABKAsyIy5jb2RlYmluZ2UuZmlyZXN0b3JlLmNvZGVjLnYxLkZpZWxkUgVmaWVsZEIkCiBjb20uY29kZWJpbmdlLmZpcmVzdG9yZS5jb2RlYy52MVABYgZwcm90bzM", [file_google_protobuf_descriptor]);
|
|
8
8
|
/**
|
|
9
9
|
* Describes the message codebinge.firestore.codec.v1.Field.
|
|
10
10
|
* Use `create(FieldSchema)` to create a new message.
|
|
@@ -61,6 +61,6 @@ export var EnumEncoding;
|
|
|
61
61
|
*/
|
|
62
62
|
export const EnumEncodingSchema = /*@__PURE__*/ enumDesc(file_codebinge_firestore_codec_v1_options, 1);
|
|
63
63
|
/**
|
|
64
|
-
* @generated from extension: codebinge.firestore.codec.v1.Field field =
|
|
64
|
+
* @generated from extension: codebinge.firestore.codec.v1.Field field = 1376;
|
|
65
65
|
*/
|
|
66
66
|
export const field = /*@__PURE__*/ extDesc(file_codebinge_firestore_codec_v1_options, 0);
|
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
|
-
* `
|
|
5
|
-
*
|
|
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.
|
|
38
|
-
*
|
|
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.
|
|
24
|
-
*
|
|
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.
|
|
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",
|
|
@@ -11,15 +11,17 @@ option java_multiple_files = true;
|
|
|
11
11
|
|
|
12
12
|
// Per-field encoding options. See docs/encoding.md §7.
|
|
13
13
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// and
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
14
|
+
// Extension number 1376 is registered to this project in protobuf's Global
|
|
15
|
+
// Extension Registry, which reserves 1376-1380 for it. Do not change it: the
|
|
16
|
+
// number is baked into every generated artifact and into every consumer .proto,
|
|
17
|
+
// and a consumer still on an old number stops seeing the annotation silently
|
|
18
|
+
// rather than failing.
|
|
19
|
+
//
|
|
20
|
+
// The remaining four are held for future extend sites. Each extend site
|
|
21
|
+
// consumes one number, so extending MessageOptions (or any other descriptor
|
|
22
|
+
// options message) takes the next one, 1377 -- not a reuse of this one.
|
|
21
23
|
extend google.protobuf.FieldOptions {
|
|
22
|
-
Field field =
|
|
24
|
+
Field field = 1376;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
message Field {
|