Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion example/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@
"@effect-firebase/client": "workspace:*",
"@example/shared": "workspace:*"
}
}
}
2 changes: 1 addition & 1 deletion example/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@
"@example/shared": "workspace:*"
},
"devDependencies": {}
}
}
2 changes: 1 addition & 1 deletion example/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@
"devDependencies": {
"effect-firebase": "workspace:*"
}
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,4 @@
"packages/*",
"example/*"
]
}
}
21 changes: 11 additions & 10 deletions packages/effect-firebase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,17 @@ class PostModel extends Model.Class<PostModel>('PostModel')({

Built-in field helpers:

| Helper | Behaviour |
| ------------------------------------------- | --------------------------------------------------------------------- |
| `Model.GeneratedByDb(schema)` | Auto-generated (e.g. IDs). Excluded from `add` and `update`. |
| `Model.DateTimeInsert` | Server timestamp on create. Excluded from `update`. |
| `Model.DateTimeUpdate` | Server timestamp on every write. |
| `Model.Reference(id, collection)` | Branded ID in app, `DocumentReference` in Firestore. |
| `Model.ReferenceAsInstance(id, collection)` | Same, but exposes `DocumentReference` in the app layer. |
| `Model.OptionalDeletable(schema)` | Optional field that can be deleted with a sentinel value. |
| `Model.Array(schema)` | Array field. |
| `Model.Field({get, add, update, json})` | Fully custom per-variant schemas. |
| Helper | Behaviour |
| ------------------------------------------- | ------------------------------------------------------------ |
| `Model.GeneratedByDb(schema)` | Auto-generated (e.g. IDs). Excluded from `add` and `update`. |
| `Model.DateTimeInsert` | Server timestamp on create. Excluded from `update`. |
| `Model.DateTimeUpdate` | Server timestamp on every write. |
| `Model.Reference(id, collection)` | Branded ID in app, `DocumentReference` in Firestore. |
| `Model.ReferenceAsInstance(id, collection)` | Same, but exposes `DocumentReference` in the app layer. |
| `Model.OptionalDeletable(schema)` | Optional field that can be deleted with a sentinel value. |
| `Model.Array(schema)` | Array field. |
| `Model.GeoPoint` | Geographic point with latitude and longitude. |
| `Model.Field({get, add, update, json})` | Fully custom per-variant schemas. |
Comment thread
fwal marked this conversation as resolved.

## Repository

Expand Down
1 change: 1 addition & 0 deletions packages/effect-firebase/src/lib/firestore/firestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './fields/array.js';

// Firestore-specific model field type helpers
export * from './model/datetime.js';
export * from './model/geopoint.js';
export * from './model/reference.js';
export * from './model/optional.js';
export * from './model/array.js';
Expand Down
106 changes: 106 additions & 0 deletions packages/effect-firebase/src/lib/firestore/model/geopoint.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Schema } from 'effect';
import { describe, expect, it } from 'vitest';
import { Model } from 'effect/unstable/schema';
import { GeoPoint } from './geopoint.js';
import { GeoPoint as GeoPointClass } from '../schema/geopoint.js';

describe('Model.GeoPoint', () => {
const PlaceId = Schema.String.pipe(Schema.brand('PlaceId'));

class TestModel extends Model.Class<TestModel>('TestModel')({
id: Model.GeneratedByDb(PlaceId),
location: GeoPoint,
}) {}

describe('select variant', () => {
it('should decode a GeoPoint instance to a GeoPoint instance', () => {
const decode = Schema.decodeUnknownSync(TestModel);
const result = decode({
id: 'place-1',
location: new GeoPointClass({
latitude: 37.7749,
longitude: -122.4194,
}),
});

expect(result.location).toBeInstanceOf(GeoPointClass);
expect(result.location.latitude).toBe(37.7749);
expect(result.location.longitude).toBe(-122.4194);
});

it('should encode a GeoPoint instance to a GeoPoint instance', () => {
const encode = Schema.encodeSync(TestModel);
const result = encode(
new TestModel({
id: PlaceId.make('place-1'),
location: new GeoPointClass({
latitude: 37.7749,
longitude: -122.4194,
}),
})
);

expect(result.location).toBeInstanceOf(GeoPointClass);
expect(result.location.latitude).toBe(37.7749);
expect(result.location.longitude).toBe(-122.4194);
});
});

describe('insert variant', () => {
it('should encode a GeoPoint instance to a GeoPoint instance', () => {
const encode = Schema.encodeSync(TestModel.insert);
const result = encode({
location: new GeoPointClass({ latitude: 51.5074, longitude: -0.1278 }),
});

expect(result.location).toBeInstanceOf(GeoPointClass);
});
});

describe('json variant', () => {
it('should decode a plain object to a GeoPoint instance', () => {
const decode = Schema.decodeUnknownSync(TestModel.json);
const result = decode({
id: 'place-1',
location: { latitude: 35.6762, longitude: 139.6503 },
});

expect(result.location).toBeInstanceOf(GeoPointClass);
expect(result.location.latitude).toBe(35.6762);
expect(result.location.longitude).toBe(139.6503);
});

it('should encode a GeoPoint instance to a plain object', () => {
const encode = Schema.encodeSync(TestModel.json);
const result = encode({
id: PlaceId.make('place-1'),
location: new GeoPointClass({ latitude: 35.6762, longitude: 139.6503 }),
});

expect(result.location).toEqual({
latitude: 35.6762,
longitude: 139.6503,
});
expect(result.location).not.toBeInstanceOf(GeoPointClass);
});

it('should survive a JSON.stringify/parse roundtrip', () => {
const encode = Schema.encodeSync(TestModel.json);
const decode = Schema.decodeUnknownSync(TestModel.json);

const original = new TestModel({
id: PlaceId.make('place-1'),
location: new GeoPointClass({
latitude: -33.8688,
longitude: 151.2093,
}),
});

const roundtripped = decode(JSON.parse(JSON.stringify(encode(original))));

expect(roundtripped.location).toBeInstanceOf(GeoPointClass);
expect(roundtripped.location.latitude).toBe(-33.8688);
expect(roundtripped.location.longitude).toBe(151.2093);
});
});
});
42 changes: 42 additions & 0 deletions packages/effect-firebase/src/lib/firestore/model/geopoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Model, VariantSchema } from 'effect/unstable/schema';
import * as FirestoreSchema from '../schema/schema.js';

/**
* A field that stores a GeoPoint in Firestore and survives JSON serialization.
*
* - App (Type): `GeoPoint` class instance
* - DB (Encoded): `GeoPoint` class instance (preserved through the driver)
* - JSON (Encoded): plain `{ latitude, longitude }` object
*
* The DB variants keep the class instance intact so the Firestore driver can
* map it to a native GeoPoint, while the JSON variants encode to a plain object
* so the value can round-trip through `JSON.stringify`/`JSON.parse` like the
* other model field types.
*
* @example
* ```ts
* import { Model } from 'effect-firebase';
*
* class PlaceModel extends Model.Class<PlaceModel>('PlaceModel')({
* id: Model.GeneratedByDb(PlaceId),
* location: Model.GeoPoint,
* }) {}
* ```
*/
export type GeoPoint = VariantSchema.Field<{
select: typeof FirestoreSchema.GeoPointInstance;
insert: typeof FirestoreSchema.GeoPointInstance;
update: typeof FirestoreSchema.GeoPointInstance;
json: typeof FirestoreSchema.GeoPoint;
jsonCreate: typeof FirestoreSchema.GeoPoint;
jsonUpdate: typeof FirestoreSchema.GeoPoint;
}>;

export const GeoPoint: GeoPoint = Model.Field({
select: FirestoreSchema.GeoPointInstance,
insert: FirestoreSchema.GeoPointInstance,
update: FirestoreSchema.GeoPointInstance,
json: FirestoreSchema.GeoPoint,
jsonCreate: FirestoreSchema.GeoPoint,
jsonUpdate: FirestoreSchema.GeoPoint,
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './array.js';
export * from './datetime.js';
export * from './geopoint.js';
export * from './optional.js';
export * from './reference.js';
export * from './repository.js';
15 changes: 15 additions & 0 deletions packages/effect-firebase/src/lib/firestore/schema/geopoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,18 @@ export class GeoPoint extends Schema.Class<GeoPoint>('GeoPoint')({
latitude: Schema.Number,
longitude: Schema.Number,
}) {}

/**
* Schema where GeoPoint class instance is both Type and Encoded.
* Using instanceOf ensures the class instance is preserved through Schema.encode.
*/
export const GeoPointInstance = Schema.instanceOf(GeoPoint, {
jsonSchema: {
type: 'object',
required: ['latitude', 'longitude'],
properties: {
latitude: { type: 'number' },
longitude: { type: 'number' },
},
},
});
Loading