From 3181c390df0a740d0c9801ad0f31487277ed96fd Mon Sep 17 00:00:00 2001 From: Maxime Amillastre Date: Wed, 10 Jul 2024 18:28:33 +0200 Subject: [PATCH 1/9] add the web implementation --- package-lock.json | 15 +++- packages/firestore/package.json | 2 +- packages/firestore/src/client.ts | 102 ++++++++++++++++++++++++++ packages/firestore/src/field-value.ts | 21 ++++++ packages/firestore/src/index.ts | 10 ++- packages/firestore/src/internals.ts | 28 +++++++ packages/firestore/src/timestamp.ts | 40 ++++++++++ 7 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 packages/firestore/src/client.ts create mode 100644 packages/firestore/src/field-value.ts create mode 100644 packages/firestore/src/internals.ts create mode 100644 packages/firestore/src/timestamp.ts diff --git a/package-lock.json b/package-lock.json index c228dc38..0fa01c5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8237,7 +8237,7 @@ "rimraf": "3.0.2", "rollup": "2.77.2", "swiftlint": "1.0.1", - "typescript": "4.1.5" + "typescript": "4.9.5" }, "peerDependencies": { "@capacitor/core": "^6.0.0", @@ -8261,6 +8261,19 @@ "node": ">=10.13.0" } }, + "packages/firestore/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "packages/functions": { "name": "@capacitor-firebase/functions", "version": "6.3.0", diff --git a/packages/firestore/package.json b/packages/firestore/package.json index 037049aa..2e75bcfb 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -70,7 +70,7 @@ "rimraf": "3.0.2", "rollup": "2.77.2", "swiftlint": "1.0.1", - "typescript": "4.1.5" + "typescript": "4.9.5" }, "peerDependencies": { "@capacitor/core": "^6.0.0", diff --git a/packages/firestore/src/client.ts b/packages/firestore/src/client.ts new file mode 100644 index 00000000..d8c78309 --- /dev/null +++ b/packages/firestore/src/client.ts @@ -0,0 +1,102 @@ +import { Timestamp as OriginalTimestamp } from 'firebase/firestore'; + +import type { DocumentSnapshot, FirebaseFirestorePlugin } from './definitions'; +import type { CustomField } from './internals'; +import { FIRESTORE_FIELD_TYPE, FIRESTORE_FIELD_VALUE } from './internals'; +import { Timestamp } from './timestamp'; + +/** + * Apply a proxy on the plugin to manage document data parsing + * @param plugin The capacitor plugin + * @returns A proxied plugin that manage parsing + */ +export function getClientPlugin( + plugin: FirebaseFirestorePlugin, +): FirebaseFirestorePlugin { + return new Proxy(plugin, { + get(target, prop) { + // Get document, collection or collection group + if ( + prop === 'getDocument' || + prop === 'getCollection' || + prop === 'getCollectionGroup' + ) { + return async function (options: any): Promise { + return parseResult(await (target[prop] as any)(options)); + }; + } + + // Listener document, collection, collection group + if ( + prop === 'addDocumentSnapshotListener' || + prop === 'addCollectionSnapshotListener' || + prop === 'addCollectionGroupSnapshotListener' + ) { + return async function (options: any, callback: any): Promise { + return (target[prop] as any)(options, (ev: any, err: any) => + callback(ev ? parseResult(ev as any) : ev, err), + ); + }; + } + + return (target as any)[prop]; + }, + }); +} + +/** + * Parse a received result + * @param res The result of a read method + * @returns The parsed result + */ +function parseResult< + T, + U extends { + snapshot?: DocumentSnapshot; + snapshots?: DocumentSnapshot[]; + }, +>(res: U): U { + if (res?.snapshot?.data) { + res.snapshot.data = parseResultDocumentData(res.snapshot.data); + } + if (res?.snapshots) { + res.snapshots.map(s => parseResultDocumentData(s)); + } + return res; +} + +/** + * Parse the document data result to convert some field values + * @param data The document data to parse + * @returns + */ +function parseResultDocumentData(data: any): any { + if (!data) { + return data; + } + + // On web, convert the firebase Timestamp into the custom one + if (data instanceof OriginalTimestamp) { + return Timestamp.fromOriginalTimestamp(data); + } + + // On native, we receive the special JSON format to convert + if (data[FIRESTORE_FIELD_TYPE]) { + const field: CustomField = data; + switch (field[FIRESTORE_FIELD_TYPE]) { + case 'timestamp': + return new Timestamp( + field[FIRESTORE_FIELD_VALUE].seconds, + field[FIRESTORE_FIELD_VALUE].nanoseconds, + ); + } + } + + if (typeof data === 'object') { + Object.keys(data).forEach(key => { + data[key] = parseResultDocumentData(data[key]); + }); + } + + return data; +} diff --git a/packages/firestore/src/field-value.ts b/packages/firestore/src/field-value.ts new file mode 100644 index 00000000..5b79e906 --- /dev/null +++ b/packages/firestore/src/field-value.ts @@ -0,0 +1,21 @@ +import type { FieldValue as OriginalFieldValue } from 'firebase/firestore'; +import { serverTimestamp as originalServerTimestamp } from 'firebase/firestore'; + +import type { CustomField } from './internals'; +import { FIRESTORE_FIELD_TYPE, FIRESTORE_FIELD_VALUE } from './internals'; + +type FieldValue = OriginalFieldValue & { toJSON?: () => any }; + +export function serverTimestamp( + ...args: Parameters +): OriginalFieldValue { + const fieldValue: FieldValue = originalServerTimestamp(...args); + fieldValue.toJSON = () => + ({ + [FIRESTORE_FIELD_TYPE]: 'fieldvalue', + [FIRESTORE_FIELD_VALUE]: { + method: 'serverTimestamp', + }, + } as CustomField); + return fieldValue; +} diff --git a/packages/firestore/src/index.ts b/packages/firestore/src/index.ts index 057ec4ec..ce37f58f 100644 --- a/packages/firestore/src/index.ts +++ b/packages/firestore/src/index.ts @@ -1,13 +1,15 @@ import { registerPlugin } from '@capacitor/core'; +import { getClientPlugin } from './client'; import type { FirebaseFirestorePlugin } from './definitions'; -const FirebaseFirestore = registerPlugin( - 'FirebaseFirestore', - { +const FirebaseFirestore = getClientPlugin( + registerPlugin('FirebaseFirestore', { web: () => import('./web').then(m => new m.FirebaseFirestoreWeb()), - }, + }), ); export * from './definitions'; +export * from './timestamp'; +export * from './field-value'; export { FirebaseFirestore }; diff --git a/packages/firestore/src/internals.ts b/packages/firestore/src/internals.ts new file mode 100644 index 00000000..05399da7 --- /dev/null +++ b/packages/firestore/src/internals.ts @@ -0,0 +1,28 @@ +/** + * The custom field type attribute key + */ +export const FIRESTORE_FIELD_TYPE = '_capacitorFirestoreFieldType'; + +/** + * The custom field value attribute key + */ +export const FIRESTORE_FIELD_VALUE = '_capacitorFirestoreFieldValue'; + +/** + * A firestore document data custom field + * Used to serialize the special Firestore fields into JSON + */ +export type CustomField = + | { + [FIRESTORE_FIELD_TYPE]: 'fieldvalue'; + [FIRESTORE_FIELD_VALUE]: { + method: string; + }; + } + | { + [FIRESTORE_FIELD_TYPE]: 'timestamp'; + [FIRESTORE_FIELD_VALUE]: { + seconds: number; + nanoseconds: number; + }; + }; diff --git a/packages/firestore/src/timestamp.ts b/packages/firestore/src/timestamp.ts new file mode 100644 index 00000000..e064e5af --- /dev/null +++ b/packages/firestore/src/timestamp.ts @@ -0,0 +1,40 @@ +import { Timestamp as OriginalTimestamp } from 'firebase/firestore'; + +import type { CustomField } from './internals'; +import { FIRESTORE_FIELD_TYPE, FIRESTORE_FIELD_VALUE } from './internals'; + +export class Timestamp extends OriginalTimestamp { + /** + * Creates a new timestamp from a JS SDK firestore timestamp. + * + * @param timestamp The Firestore timestamp. + * @returns A new `Timestamp` representing the same point in time as the given timestamp. + */ + public static fromOriginalTimestamp(timestamp: OriginalTimestamp): Timestamp { + return new Timestamp(timestamp.seconds, timestamp.nanoseconds); + } + + static now(): Timestamp { + return Timestamp.fromOriginalTimestamp(OriginalTimestamp.now()); + } + + static fromDate(date: Date): Timestamp { + return Timestamp.fromOriginalTimestamp(OriginalTimestamp.fromDate(date)); + } + + static fromMillis(milliseconds: number): Timestamp { + return Timestamp.fromOriginalTimestamp( + OriginalTimestamp.fromMillis(milliseconds), + ); + } + + public toJSON(): any { + return { + [FIRESTORE_FIELD_TYPE]: 'timestamp', + [FIRESTORE_FIELD_VALUE]: { + seconds: this.seconds, + nanoseconds: this.nanoseconds, + }, + } as CustomField; + } +} From cd2314e7a6c1cea49a1c62948fb8d99abc1e520c Mon Sep 17 00:00:00 2001 From: Maxime Amillastre Date: Wed, 17 Jul 2024 12:06:20 +0200 Subject: [PATCH 2/9] add the android implementation --- .../firestore/FirebaseFirestoreHelper.java | 29 ++++++-- .../firestore/classes/fields/FieldValue.java | 33 +++++++++ .../classes/fields/FirestoreField.java | 71 +++++++++++++++++++ .../firestore/classes/fields/Timestamp.java | 51 +++++++++++++ .../firestore/enums/FieldValueMethod.java | 26 +++++++ .../firestore/enums/FirestoreFieldType.java | 27 +++++++ 6 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/FieldValue.java create mode 100644 packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/FirestoreField.java create mode 100644 packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/Timestamp.java create mode 100644 packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/enums/FieldValueMethod.java create mode 100644 packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/enums/FirestoreFieldType.java diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestoreHelper.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestoreHelper.java index a3e18c5c..7ad62814 100644 --- a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestoreHelper.java +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestoreHelper.java @@ -10,6 +10,7 @@ import io.capawesome.capacitorjs.plugins.firebase.firestore.classes.constraints.QueryLimitConstraint; import io.capawesome.capacitorjs.plugins.firebase.firestore.classes.constraints.QueryOrderByConstraint; import io.capawesome.capacitorjs.plugins.firebase.firestore.classes.constraints.QueryStartAtConstraint; +import io.capawesome.capacitorjs.plugins.firebase.firestore.classes.fields.FirestoreField; import io.capawesome.capacitorjs.plugins.firebase.firestore.interfaces.QueryNonFilterConstraint; import java.util.ArrayList; import java.util.HashMap; @@ -45,7 +46,7 @@ public static JSObject createJSObjectFromMap(@Nullable Map map) } else if (value instanceof Map) { value = createJSObjectFromMap((Map) value); } - object.put(key, value); + object.put(key, parseObject(value)); } return object; } @@ -54,7 +55,7 @@ public static Object createObjectFromJSValue(Object value) throws JSONException if (value.toString().equals("null")) { return null; } else if (value instanceof JSONObject) { - return createHashMapFromJSONObject((JSONObject) value); + return createObjectFromJSONObject((JSONObject) value); } else if (value instanceof JSONArray) { return createArrayListFromJSONArray((JSONArray) value); } else { @@ -108,7 +109,7 @@ private static ArrayList createArrayListFromJSONArray(JSONArray array) t for (int x = 0; x < array.length(); x++) { Object value = array.get(x); if (value instanceof JSONObject) { - value = createHashMapFromJSONObject((JSONObject) value); + value = createObjectFromJSONObject((JSONObject) value); } else if (value instanceof JSONArray) { value = createArrayListFromJSONArray((JSONArray) value); } @@ -123,7 +124,7 @@ private static JSArray createJSArrayFromArrayList(ArrayList arrayList) { if (value instanceof Map) { value = createJSObjectFromMap((Map) value); } - array.put(value); + array.put(parseObject(value)); } return array; } @@ -134,4 +135,24 @@ public static JSObject createSnapshotMetadataResult(DocumentSnapshot snapshot) { obj.put("hasPendingWrites", snapshot.getMetadata().hasPendingWrites()); return obj; } + + private static Object createObjectFromJSONObject(@NonNull JSONObject object) throws JSONException { + if (FirestoreField.isFirestoreField(object)) { + FirestoreField field = FirestoreField.fromJSONObject(object); + return field.getField(); + } + + return createHashMapFromJSONObject(object); + } + + /** + * Parse an object to return it's firestore field value. Else return the same object. + */ + private static Object parseObject(@NonNull Object object) { + try { + return FirestoreField.fromObject(object).getJSObject(); + } catch (Exception e) {} + + return object; + } } diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/FieldValue.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/FieldValue.java new file mode 100644 index 00000000..45dc93a9 --- /dev/null +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/FieldValue.java @@ -0,0 +1,33 @@ +package io.capawesome.capacitorjs.plugins.firebase.firestore.classes.fields; + +import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +import io.capawesome.capacitorjs.plugins.firebase.firestore.enums.FieldValueMethod; + +public class FieldValue { + + @NonNull + FieldValueMethod method; + + public FieldValue(@NonNull FieldValueMethod method) { + this.method = method; + } + + public static FieldValue fromJSONObject(@NonNull JSONObject value) throws JSONException { + return new FieldValue( + FieldValueMethod.fromString(value.getString("method")) + ); + } + + public Object getField() { + switch (method) { + case CREATE_SERVER_TIMESTAMP: + return com.google.firebase.firestore.FieldValue.serverTimestamp(); + default: + return null; + } + } +} diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/FirestoreField.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/FirestoreField.java new file mode 100644 index 00000000..0ac60dd4 --- /dev/null +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/FirestoreField.java @@ -0,0 +1,71 @@ +package io.capawesome.capacitorjs.plugins.firebase.firestore.classes.fields; + +import androidx.annotation.NonNull; + +import com.getcapacitor.JSObject; + +import org.json.JSONException; +import org.json.JSONObject; + +import io.capawesome.capacitorjs.plugins.firebase.firestore.enums.FirestoreFieldType; + +public class FirestoreField { + + @NonNull + private FirestoreFieldType type; + + @NonNull + private JSONObject value; + + private static final String FIRESTORE_FIELD_TYPE = "_capacitorFirestoreFieldType"; + private static final String FIRESTORE_FIELD_VALUE = "_capacitorFirestoreFieldValue"; + + /** + * Is a JSONObject a serialized Firestore field + * @param firestoreFieldData + * @return + */ + public static boolean isFirestoreField(JSONObject firestoreFieldData) { + if (!firestoreFieldData.has(FIRESTORE_FIELD_TYPE)) { + return false; + } + return true; + } + + public FirestoreField(FirestoreFieldType type, JSONObject value) { + this.type = type; + this.value = value; + } + + public static FirestoreField fromJSONObject(JSONObject data) throws JSONException { + FirestoreFieldType type = FirestoreFieldType.fromString((String) data.get(FIRESTORE_FIELD_TYPE)); + JSONObject value = (JSONObject) data.get(FIRESTORE_FIELD_VALUE); + return new FirestoreField(type, value); + } + + public static FirestoreField fromObject(Object object) throws IllegalArgumentException { + if (object instanceof com.google.firebase.Timestamp) { + Timestamp timestamp = Timestamp.fromFirestore((com.google.firebase.Timestamp) object); + return new FirestoreField(FirestoreFieldType.TIMESTAMP, timestamp.getValue()); + } + throw new IllegalArgumentException("The provided object is not a firestore field"); + } + + public Object getField() throws JSONException { + switch(type) { + case FIELD_VALUE: + return FieldValue.fromJSONObject(value).getField(); + case TIMESTAMP: + return Timestamp.fromJSONObject(value).getField(); + default: + return null; + } + } + + public JSObject getJSObject() throws JSONException { + JSObject object = new JSObject(); + object.put(FIRESTORE_FIELD_TYPE, type.toString()); + object.put(FIRESTORE_FIELD_VALUE, JSObject.fromJSONObject(value)); + return object; + } +} diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/Timestamp.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/Timestamp.java new file mode 100644 index 00000000..9b9be1ed --- /dev/null +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/Timestamp.java @@ -0,0 +1,51 @@ +package io.capawesome.capacitorjs.plugins.firebase.firestore.classes.fields; + +import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +public class Timestamp { + + @NonNull + long seconds; + + @NonNull + int nanoseconds; + + public Timestamp(@NonNull long seconds, @NonNull int nanoseconds) { + this.seconds = seconds; + this.nanoseconds = nanoseconds; + } + + public static Timestamp fromJSONObject(@NonNull JSONObject value) throws JSONException { + return new Timestamp( + ((Number) value.get("seconds")).longValue(), + (int) value.get("nanoseconds") + ); + } + + public static Timestamp fromFirestore(@NonNull com.google.firebase.Timestamp timestamp) { + return new Timestamp( + timestamp.getSeconds(), + timestamp.getNanoseconds() + ); + } + + @NonNull + public JSONObject getValue() { + JSONObject value = new JSONObject(); + try { + value.put("seconds", seconds); + value.put("nanoseconds", nanoseconds); + } catch (JSONException e) {} + return value; + } + + public com.google.firebase.Timestamp getField() throws JSONException { + return new com.google.firebase.Timestamp( + seconds, + nanoseconds + ); + } +} diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/enums/FieldValueMethod.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/enums/FieldValueMethod.java new file mode 100644 index 00000000..a78b55cf --- /dev/null +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/enums/FieldValueMethod.java @@ -0,0 +1,26 @@ +package io.capawesome.capacitorjs.plugins.firebase.firestore.enums; + +public enum FieldValueMethod { + CREATE_SERVER_TIMESTAMP("serverTimestamp"); + + private String value; + + private FieldValueMethod(String value) { + this.value = value; + } + + private String getValue() { + return value; + } + + @Override + public String toString() { + return this.getValue(); + } + + public static FieldValueMethod fromString(String value) { + for(FieldValueMethod v : values()) + if(v.getValue().equalsIgnoreCase(value)) return v; + throw new IllegalArgumentException(); + } +} diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/enums/FirestoreFieldType.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/enums/FirestoreFieldType.java new file mode 100644 index 00000000..47fc8b30 --- /dev/null +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/enums/FirestoreFieldType.java @@ -0,0 +1,27 @@ +package io.capawesome.capacitorjs.plugins.firebase.firestore.enums; + +public enum FirestoreFieldType { + FIELD_VALUE("fieldvalue"), + TIMESTAMP("timestamp"); + + private String value; + + private FirestoreFieldType(String value) { + this.value = value; + } + + private String getValue() { + return value; + } + + @Override + public String toString() { + return this.getValue(); + } + + public static FirestoreFieldType fromString(String value) { + for(FirestoreFieldType v : values()) + if(v.getValue().equalsIgnoreCase(value)) return v; + throw new IllegalArgumentException(); + } +} From 600011013b06ae723e3d50163195dcfa37450a66 Mon Sep 17 00:00:00 2001 From: mamillastre Date: Mon, 6 Jan 2025 11:10:22 +0100 Subject: [PATCH 3/9] add all the firestore field value on the web platform --- packages/firestore/README.md | 17 +++++ packages/firestore/src/client.ts | 6 +- packages/firestore/src/field-value.ts | 105 +++++++++++++++++++++++++- packages/firestore/src/internals.ts | 31 ++++---- packages/firestore/src/timestamp.ts | 44 +++++++++++ 5 files changed, 183 insertions(+), 20 deletions(-) diff --git a/packages/firestore/README.md b/packages/firestore/README.md index 601e0253..9b8c7fe5 100644 --- a/packages/firestore/README.md +++ b/packages/firestore/README.md @@ -629,6 +629,9 @@ Remove all listeners for this plugin. #### DocumentData +Document data (for use with {@link @firebase/firestore/lite#(setDoc:1)}) consists of fields mapped to +values. + #### SetDocumentOptions @@ -844,6 +847,9 @@ Remove all listeners for this plugin. #### QueryFilterConstraint +`QueryFilterConstraint` is a helper union type that represents +{@link QueryFieldFilterConstraint} and {@link QueryCompositeFilterConstraint}. + QueryFieldFilterConstraint | QueryCompositeFilterConstraint @@ -854,11 +860,22 @@ Remove all listeners for this plugin. #### QueryNonFilterConstraint +`QueryNonFilterConstraint` is a helper union type that represents +QueryConstraints which are used to narrow or order the set of documents, +but that do not explicitly filter on a document field. +`QueryNonFilterConstraint`s are created by invoking {@link orderBy}, +{@link (startAt:1)}, {@link (startAfter:1)}, {@link (endBefore:1)}, {@link (endAt:1)}, +{@link limit} or {@link limitToLast} and can then be passed to {@link (query:1)} +to create a new query instance that also contains the `QueryConstraint`. + QueryOrderByConstraint | QueryLimitConstraint | QueryStartAtConstraint | QueryEndAtConstraint #### OrderByDirection +The direction of a {@link orderBy} clause is specified as 'desc' or 'asc' +(descending or ascending). + 'desc' | 'asc' diff --git a/packages/firestore/src/client.ts b/packages/firestore/src/client.ts index d8c78309..7c1cefa4 100644 --- a/packages/firestore/src/client.ts +++ b/packages/firestore/src/client.ts @@ -1,7 +1,7 @@ import { Timestamp as OriginalTimestamp } from 'firebase/firestore'; import type { DocumentSnapshot, FirebaseFirestorePlugin } from './definitions'; -import type { CustomField } from './internals'; +import type { CustomField, CustomTimestamp } from './internals'; import { FIRESTORE_FIELD_TYPE, FIRESTORE_FIELD_VALUE } from './internals'; import { Timestamp } from './timestamp'; @@ -86,8 +86,8 @@ function parseResultDocumentData(data: any): any { switch (field[FIRESTORE_FIELD_TYPE]) { case 'timestamp': return new Timestamp( - field[FIRESTORE_FIELD_VALUE].seconds, - field[FIRESTORE_FIELD_VALUE].nanoseconds, + (field as CustomTimestamp)[FIRESTORE_FIELD_VALUE].seconds, + (field as CustomTimestamp)[FIRESTORE_FIELD_VALUE].nanoseconds, ); } } diff --git a/packages/firestore/src/field-value.ts b/packages/firestore/src/field-value.ts index 5b79e906..fe894a0e 100644 --- a/packages/firestore/src/field-value.ts +++ b/packages/firestore/src/field-value.ts @@ -1,20 +1,119 @@ import type { FieldValue as OriginalFieldValue } from 'firebase/firestore'; -import { serverTimestamp as originalServerTimestamp } from 'firebase/firestore'; +import { + serverTimestamp as originalServerTimestamp, + arrayRemove as originalArrayRemove, + arrayUnion as originalArrayUnion, + deleteField as originalDeleteField, + increment as originalIncrement, +} from 'firebase/firestore'; import type { CustomField } from './internals'; import { FIRESTORE_FIELD_TYPE, FIRESTORE_FIELD_VALUE } from './internals'; type FieldValue = OriginalFieldValue & { toJSON?: () => any }; +/** + * Returns a special value that can be used with `setDocument()` or + * `updateDocument()` that tells the server to remove the given elements from any + * array value that already exists on the server. All instances of each element + * specified will be removed from the array. If the field being modified is not + * already an array it will be overwritten with an empty array. + * + * @since 7.0.0 + * @param elements - The elements to remove from the array. + * @returns The `FieldValue` sentinel for use in a call to `setDocument()` or + * `updateDocument()` + */ +export function arrayRemove( + ...args: Parameters +): OriginalFieldValue { + return getFieldValue('arrayRemove', originalArrayRemove, args); +} + +/** + * Returns a special value that can be used with `setDocument()` or + * `updateDocument()` that tells the server to union the given elements with any array + * value that already exists on the server. Each specified element that doesn't + * already exist in the array will be added to the end. If the field being + * modified is not already an array it will be overwritten with an array + * containing exactly the specified elements. + * + * @since 7.0.0 + * @param elements - The elements to union into the array. + * @returns The `FieldValue` sentinel for use in a call to `setDocument()` or + * `updateDocument()`. + */ +export function arrayUnion( + ...args: Parameters +): OriginalFieldValue { + return getFieldValue('arrayUnion', originalArrayUnion, args); +} + +/** + * Returns a sentinel for use with `updateDocument()` or + * `setDocument()` with `{merge: true}` to mark a field for deletion. + * + * @since 7.0.0 + */ +export function deleteField( + ...args: Parameters +): OriginalFieldValue { + return getFieldValue('deleteField', originalDeleteField, args); +} + +/** + * Returns a special value that can be used with `setDocument()` or + * `updateDocument()` that tells the server to increment the field's current value by + * the given value. + * + * If either the operand or the current field value uses floating point + * precision, all arithmetic follows IEEE 754 semantics. If both values are + * integers, values outside of JavaScript's safe number range + * (`Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`) are also subject to + * precision loss. Furthermore, once processed by the Firestore backend, all + * integer operations are capped between -2^63 and 2^63-1. + * + * If the current field value is not of type `number`, or if the field does not + * yet exist, the transformation sets the field to the given value. + * + * @since 7.0.0 + * @param n - The value to increment by. + * @returns The `FieldValue` sentinel for use in a call to `setDocument()` or + * `updateDocument()` + */ +export function increment( + ...args: Parameters +): OriginalFieldValue { + return getFieldValue('increment', originalIncrement, args); +} + +/** + * Returns a sentinel used with `setDocument()` or `updateDocument()` to + * include a server-generated timestamp in the written data. + * + * @since 7.0.0 + */ export function serverTimestamp( ...args: Parameters ): OriginalFieldValue { - const fieldValue: FieldValue = originalServerTimestamp(...args); + return getFieldValue('serverTimestamp', originalServerTimestamp, args); +} + +/** + * Build the custom FieldVallue + */ +function getFieldValue( + fieldKey: string, + method: (...args: any) => FieldValue, + args: any[], +): OriginalFieldValue { + const fieldValue: FieldValue = method(...args); fieldValue.toJSON = () => ({ [FIRESTORE_FIELD_TYPE]: 'fieldvalue', [FIRESTORE_FIELD_VALUE]: { - method: 'serverTimestamp', + method: fieldKey, + args, }, } as CustomField); return fieldValue; diff --git a/packages/firestore/src/internals.ts b/packages/firestore/src/internals.ts index 05399da7..86653186 100644 --- a/packages/firestore/src/internals.ts +++ b/packages/firestore/src/internals.ts @@ -8,21 +8,24 @@ export const FIRESTORE_FIELD_TYPE = '_capacitorFirestoreFieldType'; */ export const FIRESTORE_FIELD_VALUE = '_capacitorFirestoreFieldValue'; +export type CustomFieldValue = { + [FIRESTORE_FIELD_TYPE]: 'fieldvalue'; + [FIRESTORE_FIELD_VALUE]: { + method: string; + args?: any; + }; +}; + +export type CustomTimestamp = { + [FIRESTORE_FIELD_TYPE]: 'timestamp'; + [FIRESTORE_FIELD_VALUE]: { + seconds: number; + nanoseconds: number; + }; +}; + /** * A firestore document data custom field * Used to serialize the special Firestore fields into JSON */ -export type CustomField = - | { - [FIRESTORE_FIELD_TYPE]: 'fieldvalue'; - [FIRESTORE_FIELD_VALUE]: { - method: string; - }; - } - | { - [FIRESTORE_FIELD_TYPE]: 'timestamp'; - [FIRESTORE_FIELD_VALUE]: { - seconds: number; - nanoseconds: number; - }; - }; +export type CustomField = CustomFieldValue | CustomTimestamp; diff --git a/packages/firestore/src/timestamp.ts b/packages/firestore/src/timestamp.ts index e064e5af..d49d1dcb 100644 --- a/packages/firestore/src/timestamp.ts +++ b/packages/firestore/src/timestamp.ts @@ -3,6 +3,22 @@ import { Timestamp as OriginalTimestamp } from 'firebase/firestore'; import type { CustomField } from './internals'; import { FIRESTORE_FIELD_TYPE, FIRESTORE_FIELD_VALUE } from './internals'; +/** + * A `Timestamp` represents a point in time independent of any time zone or + * calendar, represented as seconds and fractions of seconds at nanosecond + * resolution in UTC Epoch time. + * + * It is encoded using the Proleptic Gregorian Calendar which extends the + * Gregorian calendar backwards to year one. It is encoded assuming all minutes + * are 60 seconds long, i.e. leap seconds are "smeared" so that no leap second + * table is needed for interpretation. Range is from 0001-01-01T00:00:00Z to + * 9999-12-31T23:59:59.999999999Z. + * + * For examples and further specifications, refer to the + * {@link https://github.com/google/protobuf/blob/master/src/google/protobuf/timestamp.proto | Timestamp definition}. + * + * @since 7.0.0 + */ export class Timestamp extends OriginalTimestamp { /** * Creates a new timestamp from a JS SDK firestore timestamp. @@ -14,20 +30,48 @@ export class Timestamp extends OriginalTimestamp { return new Timestamp(timestamp.seconds, timestamp.nanoseconds); } + /** + * Creates a new timestamp with the current date, with millisecond precision. + * + * @since 7.0.0 + * @returns a new timestamp representing the current date. + */ static now(): Timestamp { return Timestamp.fromOriginalTimestamp(OriginalTimestamp.now()); } + /** + * Creates a new timestamp from the given date. + * + * @since 7.0.0 + * @param date - The date to initialize the `Timestamp` from. + * @returns A new `Timestamp` representing the same point in time as the given + * date. + */ static fromDate(date: Date): Timestamp { return Timestamp.fromOriginalTimestamp(OriginalTimestamp.fromDate(date)); } + /** + * Creates a new timestamp from the given number of milliseconds. + * + * @since 7.0.0 + * @param milliseconds - Number of milliseconds since Unix epoch + * 1970-01-01T00:00:00Z. + * @returns A new `Timestamp` representing the same point in time as the given + * number of milliseconds. + */ static fromMillis(milliseconds: number): Timestamp { return Timestamp.fromOriginalTimestamp( OriginalTimestamp.fromMillis(milliseconds), ); } + /** + * Returns a JSON-serializable representation of this `Timestamp`. + * + * @since 7.0.0 + */ public toJSON(): any { return { [FIRESTORE_FIELD_TYPE]: 'timestamp', From a5b96b69d8397877860ce0eecef94c5c066af12a Mon Sep 17 00:00:00 2001 From: mamillastre Date: Mon, 6 Jan 2025 16:17:13 +0100 Subject: [PATCH 4/9] add all the firestore field value on the android platform --- .../firestore/FirebaseFirestoreHelper.java | 15 ++++---- .../firestore/classes/fields/FieldValue.java | 35 +++++++++++++------ .../classes/fields/FirestoreField.java | 29 ++++++--------- .../firestore/classes/fields/Timestamp.java | 20 +++-------- .../firestore/enums/FieldValueMethod.java | 14 +++++--- .../firestore/enums/FirestoreFieldType.java | 8 +++-- 6 files changed, 62 insertions(+), 59 deletions(-) diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestoreHelper.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestoreHelper.java index 7ad62814..ee67d248 100644 --- a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestoreHelper.java +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestoreHelper.java @@ -104,7 +104,7 @@ public static QueryNonFilterConstraint[] createQueryNonFilterConstraintArrayFrom } } - private static ArrayList createArrayListFromJSONArray(JSONArray array) throws JSONException { + public static ArrayList createArrayListFromJSONArray(JSONArray array) throws JSONException { ArrayList arrayList = new ArrayList<>(); for (int x = 0; x < array.length(); x++) { Object value = array.get(x); @@ -135,7 +135,7 @@ public static JSObject createSnapshotMetadataResult(DocumentSnapshot snapshot) { obj.put("hasPendingWrites", snapshot.getMetadata().hasPendingWrites()); return obj; } - + private static Object createObjectFromJSONObject(@NonNull JSONObject object) throws JSONException { if (FirestoreField.isFirestoreField(object)) { FirestoreField field = FirestoreField.fromJSONObject(object); @@ -148,11 +148,14 @@ private static Object createObjectFromJSONObject(@NonNull JSONObject object) thr /** * Parse an object to return it's firestore field value. Else return the same object. */ - private static Object parseObject(@NonNull Object object) { + private static Object parseObject(Object object) { + if (object == null) { + return null; + } try { return FirestoreField.fromObject(object).getJSObject(); - } catch (Exception e) {} - - return object; + } catch (Exception e) { + return object; + } } } diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/FieldValue.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/FieldValue.java index 45dc93a9..9de19197 100644 --- a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/FieldValue.java +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/FieldValue.java @@ -1,33 +1,46 @@ package io.capawesome.capacitorjs.plugins.firebase.firestore.classes.fields; -import androidx.annotation.NonNull; +import static com.google.firebase.firestore.FieldValue.arrayRemove; +import static com.google.firebase.firestore.FieldValue.arrayUnion; +import static com.google.firebase.firestore.FieldValue.delete; +import static com.google.firebase.firestore.FieldValue.increment; +import static com.google.firebase.firestore.FieldValue.serverTimestamp; +import androidx.annotation.NonNull; +import io.capawesome.capacitorjs.plugins.firebase.firestore.FirebaseFirestoreHelper; +import io.capawesome.capacitorjs.plugins.firebase.firestore.enums.FieldValueMethod; import org.json.JSONException; import org.json.JSONObject; -import io.capawesome.capacitorjs.plugins.firebase.firestore.enums.FieldValueMethod; - public class FieldValue { @NonNull FieldValueMethod method; - public FieldValue(@NonNull FieldValueMethod method) { + Object[] args; + + public FieldValue(@NonNull FieldValueMethod method, Object[] args) { this.method = method; + this.args = args; } public static FieldValue fromJSONObject(@NonNull JSONObject value) throws JSONException { return new FieldValue( - FieldValueMethod.fromString(value.getString("method")) + FieldValueMethod.fromString(value.getString("method")), + FirebaseFirestoreHelper.createArrayListFromJSONArray(value.getJSONArray("args")).toArray() ); } public Object getField() { - switch (method) { - case CREATE_SERVER_TIMESTAMP: - return com.google.firebase.firestore.FieldValue.serverTimestamp(); - default: - return null; - } + return switch (method) { + case ARRAY_REMOVE -> arrayRemove(this.args); + case ARRAY_UNION -> arrayUnion(this.args); + case DELETE_FIELD -> delete(); + case INCREMENT -> this.args[0] instanceof Double + ? increment((Double) this.args[0]) + : increment(((Integer) this.args[0]).longValue()); + case SERVER_TIMESTAMP -> serverTimestamp(); + default -> null; + }; } } diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/FirestoreField.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/FirestoreField.java index 0ac60dd4..d8eb9c46 100644 --- a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/FirestoreField.java +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/FirestoreField.java @@ -1,21 +1,18 @@ package io.capawesome.capacitorjs.plugins.firebase.firestore.classes.fields; import androidx.annotation.NonNull; - import com.getcapacitor.JSObject; - +import io.capawesome.capacitorjs.plugins.firebase.firestore.enums.FirestoreFieldType; import org.json.JSONException; import org.json.JSONObject; -import io.capawesome.capacitorjs.plugins.firebase.firestore.enums.FirestoreFieldType; - public class FirestoreField { @NonNull - private FirestoreFieldType type; + private final FirestoreFieldType type; @NonNull - private JSONObject value; + private final JSONObject value; private static final String FIRESTORE_FIELD_TYPE = "_capacitorFirestoreFieldType"; private static final String FIRESTORE_FIELD_VALUE = "_capacitorFirestoreFieldValue"; @@ -26,13 +23,10 @@ public class FirestoreField { * @return */ public static boolean isFirestoreField(JSONObject firestoreFieldData) { - if (!firestoreFieldData.has(FIRESTORE_FIELD_TYPE)) { - return false; - } - return true; + return firestoreFieldData.has(FIRESTORE_FIELD_TYPE); } - public FirestoreField(FirestoreFieldType type, JSONObject value) { + public FirestoreField(@NonNull FirestoreFieldType type, @NonNull JSONObject value) { this.type = type; this.value = value; } @@ -52,14 +46,11 @@ public static FirestoreField fromObject(Object object) throws IllegalArgumentExc } public Object getField() throws JSONException { - switch(type) { - case FIELD_VALUE: - return FieldValue.fromJSONObject(value).getField(); - case TIMESTAMP: - return Timestamp.fromJSONObject(value).getField(); - default: - return null; - } + return switch (type) { + case FIELD_VALUE -> FieldValue.fromJSONObject(value).getField(); + case TIMESTAMP -> Timestamp.fromJSONObject(value).getField(); + default -> null; + }; } public JSObject getJSObject() throws JSONException { diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/Timestamp.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/Timestamp.java index 9b9be1ed..c957cddc 100644 --- a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/Timestamp.java +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/fields/Timestamp.java @@ -1,35 +1,26 @@ package io.capawesome.capacitorjs.plugins.firebase.firestore.classes.fields; import androidx.annotation.NonNull; - import org.json.JSONException; import org.json.JSONObject; public class Timestamp { - @NonNull long seconds; - @NonNull int nanoseconds; - public Timestamp(@NonNull long seconds, @NonNull int nanoseconds) { + public Timestamp(long seconds, int nanoseconds) { this.seconds = seconds; this.nanoseconds = nanoseconds; } public static Timestamp fromJSONObject(@NonNull JSONObject value) throws JSONException { - return new Timestamp( - ((Number) value.get("seconds")).longValue(), - (int) value.get("nanoseconds") - ); + return new Timestamp(((Number) value.get("seconds")).longValue(), (int) value.get("nanoseconds")); } public static Timestamp fromFirestore(@NonNull com.google.firebase.Timestamp timestamp) { - return new Timestamp( - timestamp.getSeconds(), - timestamp.getNanoseconds() - ); + return new Timestamp(timestamp.getSeconds(), timestamp.getNanoseconds()); } @NonNull @@ -43,9 +34,6 @@ public JSONObject getValue() { } public com.google.firebase.Timestamp getField() throws JSONException { - return new com.google.firebase.Timestamp( - seconds, - nanoseconds - ); + return new com.google.firebase.Timestamp(seconds, nanoseconds); } } diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/enums/FieldValueMethod.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/enums/FieldValueMethod.java index a78b55cf..b9e92ef6 100644 --- a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/enums/FieldValueMethod.java +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/enums/FieldValueMethod.java @@ -1,9 +1,15 @@ package io.capawesome.capacitorjs.plugins.firebase.firestore.enums; +import androidx.annotation.NonNull; + public enum FieldValueMethod { - CREATE_SERVER_TIMESTAMP("serverTimestamp"); + ARRAY_REMOVE("arrayRemove"), + ARRAY_UNION("arrayUnion"), + DELETE_FIELD("deleteField"), + INCREMENT("increment"), + SERVER_TIMESTAMP("serverTimestamp"); - private String value; + private final String value; private FieldValueMethod(String value) { this.value = value; @@ -13,14 +19,14 @@ private String getValue() { return value; } + @NonNull @Override public String toString() { return this.getValue(); } public static FieldValueMethod fromString(String value) { - for(FieldValueMethod v : values()) - if(v.getValue().equalsIgnoreCase(value)) return v; + for (FieldValueMethod v : values()) if (v.getValue().equalsIgnoreCase(value)) return v; throw new IllegalArgumentException(); } } diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/enums/FirestoreFieldType.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/enums/FirestoreFieldType.java index 47fc8b30..55f77935 100644 --- a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/enums/FirestoreFieldType.java +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/enums/FirestoreFieldType.java @@ -1,10 +1,12 @@ package io.capawesome.capacitorjs.plugins.firebase.firestore.enums; +import androidx.annotation.NonNull; + public enum FirestoreFieldType { FIELD_VALUE("fieldvalue"), TIMESTAMP("timestamp"); - private String value; + private final String value; private FirestoreFieldType(String value) { this.value = value; @@ -14,14 +16,14 @@ private String getValue() { return value; } + @NonNull @Override public String toString() { return this.getValue(); } public static FirestoreFieldType fromString(String value) { - for(FirestoreFieldType v : values()) - if(v.getValue().equalsIgnoreCase(value)) return v; + for (FirestoreFieldType v : values()) if (v.getValue().equalsIgnoreCase(value)) return v; throw new IllegalArgumentException(); } } From faf71633ec180262f8cc1fa8f0758c24a18cf660 Mon Sep 17 00:00:00 2001 From: mamillastre Date: Thu, 9 Jan 2025 10:31:39 +0100 Subject: [PATCH 5/9] fix the ios plugin calls --- packages/firestore/src/client.ts | 63 ++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/packages/firestore/src/client.ts b/packages/firestore/src/client.ts index 7c1cefa4..d5ad6be4 100644 --- a/packages/firestore/src/client.ts +++ b/packages/firestore/src/client.ts @@ -1,6 +1,12 @@ +import { Capacitor } from '@capacitor/core'; +import type { DocumentData } from 'firebase/firestore'; import { Timestamp as OriginalTimestamp } from 'firebase/firestore'; -import type { DocumentSnapshot, FirebaseFirestorePlugin } from './definitions'; +import type { + DocumentSnapshot, + FirebaseFirestorePlugin, + WriteBatchOptions, +} from './definitions'; import type { CustomField, CustomTimestamp } from './internals'; import { FIRESTORE_FIELD_TYPE, FIRESTORE_FIELD_VALUE } from './internals'; import { Timestamp } from './timestamp'; @@ -14,7 +20,9 @@ export function getClientPlugin( plugin: FirebaseFirestorePlugin, ): FirebaseFirestorePlugin { return new Proxy(plugin, { - get(target, prop) { + get(target, prop, receiver) { + const getProperty = Reflect.get(target, prop, receiver); + // Get document, collection or collection group if ( prop === 'getDocument' || @@ -22,7 +30,28 @@ export function getClientPlugin( prop === 'getCollectionGroup' ) { return async function (options: any): Promise { - return parseResult(await (target[prop] as any)(options)); + return parseResult(await getProperty(options)); + }; + } + + // Add, update, set document + if ( + prop === 'addDocument' || + prop === 'setDocument' || + prop === 'updateDocument' + ) { + return async function (options: any): Promise { + return getProperty(formatOptionsData(options)); + }; + } + + // Write batch + if (prop === 'writeBatch') { + return async function (options: WriteBatchOptions): Promise { + options.operations = options.operations.map(operation => + formatOptionsData(operation), + ); + return getProperty(options); }; } @@ -33,17 +62,30 @@ export function getClientPlugin( prop === 'addCollectionGroupSnapshotListener' ) { return async function (options: any, callback: any): Promise { - return (target[prop] as any)(options, (ev: any, err: any) => + return getProperty(options, (ev: any, err: any) => callback(ev ? parseResult(ev as any) : ev, err), ); }; } - return (target as any)[prop]; + return getProperty; }, }); } +/** + * Format the options data to be passed to the native plugin + * @param options The options to format + * @returns The formated options + */ +function formatOptionsData(options: T): T { + if (Capacitor.isNativePlatform() && options.data) { + // Force the data to be serialized in JSON + options.data = JSON.parse(JSON.stringify(options.data)); + } + return options; +} + /** * Parse a received result * @param res The result of a read method @@ -83,12 +125,11 @@ function parseResultDocumentData(data: any): any { // On native, we receive the special JSON format to convert if (data[FIRESTORE_FIELD_TYPE]) { const field: CustomField = data; - switch (field[FIRESTORE_FIELD_TYPE]) { - case 'timestamp': - return new Timestamp( - (field as CustomTimestamp)[FIRESTORE_FIELD_VALUE].seconds, - (field as CustomTimestamp)[FIRESTORE_FIELD_VALUE].nanoseconds, - ); + if (field[FIRESTORE_FIELD_TYPE] === 'timestamp') { + return new Timestamp( + (field as CustomTimestamp)[FIRESTORE_FIELD_VALUE].seconds, + (field as CustomTimestamp)[FIRESTORE_FIELD_VALUE].nanoseconds, + ); } } From d8d1ea16ee3f041257e76cace9c49b08e0a024b6 Mon Sep 17 00:00:00 2001 From: mamillastre Date: Thu, 9 Jan 2025 14:25:16 +0100 Subject: [PATCH 6/9] enforce the type checking by replacing the plugin proxy by a class --- packages/firestore/src/client.ts | 197 ++++++++++++++++++++----------- packages/firestore/src/index.ts | 4 +- 2 files changed, 127 insertions(+), 74 deletions(-) diff --git a/packages/firestore/src/client.ts b/packages/firestore/src/client.ts index d5ad6be4..054a7d26 100644 --- a/packages/firestore/src/client.ts +++ b/packages/firestore/src/client.ts @@ -1,10 +1,29 @@ import { Capacitor } from '@capacitor/core'; -import type { DocumentData } from 'firebase/firestore'; import { Timestamp as OriginalTimestamp } from 'firebase/firestore'; import type { - DocumentSnapshot, + AddCollectionGroupSnapshotListenerCallback, + AddCollectionGroupSnapshotListenerOptions, + AddCollectionSnapshotListenerCallback, + AddCollectionSnapshotListenerOptions, + AddDocumentOptions, + AddDocumentResult, + AddDocumentSnapshotListenerCallback, + AddDocumentSnapshotListenerOptions, + CallbackId, + DeleteDocumentOptions, + DocumentData, FirebaseFirestorePlugin, + GetCollectionGroupOptions, + GetCollectionGroupResult, + GetCollectionOptions, + GetCollectionResult, + GetDocumentOptions, + GetDocumentResult, + RemoveSnapshotListenerOptions, + SetDocumentOptions, + UpdateDocumentOptions, + UseEmulatorOptions, WriteBatchOptions, } from './definitions'; import type { CustomField, CustomTimestamp } from './internals'; @@ -12,65 +31,95 @@ import { FIRESTORE_FIELD_TYPE, FIRESTORE_FIELD_VALUE } from './internals'; import { Timestamp } from './timestamp'; /** - * Apply a proxy on the plugin to manage document data parsing - * @param plugin The capacitor plugin - * @returns A proxied plugin that manage parsing + * The plugin client that manage all the data parsing / format between the web and the native platform */ -export function getClientPlugin( - plugin: FirebaseFirestorePlugin, -): FirebaseFirestorePlugin { - return new Proxy(plugin, { - get(target, prop, receiver) { - const getProperty = Reflect.get(target, prop, receiver); +export class FirebaseFirestoreClient implements FirebaseFirestorePlugin { + private readonly plugin: FirebaseFirestorePlugin; - // Get document, collection or collection group - if ( - prop === 'getDocument' || - prop === 'getCollection' || - prop === 'getCollectionGroup' - ) { - return async function (options: any): Promise { - return parseResult(await getProperty(options)); - }; - } - - // Add, update, set document - if ( - prop === 'addDocument' || - prop === 'setDocument' || - prop === 'updateDocument' - ) { - return async function (options: any): Promise { - return getProperty(formatOptionsData(options)); - }; - } - - // Write batch - if (prop === 'writeBatch') { - return async function (options: WriteBatchOptions): Promise { - options.operations = options.operations.map(operation => - formatOptionsData(operation), - ); - return getProperty(options); - }; - } - - // Listener document, collection, collection group - if ( - prop === 'addDocumentSnapshotListener' || - prop === 'addCollectionSnapshotListener' || - prop === 'addCollectionGroupSnapshotListener' - ) { - return async function (options: any, callback: any): Promise { - return getProperty(options, (ev: any, err: any) => - callback(ev ? parseResult(ev as any) : ev, err), - ); - }; - } + constructor(plugin: FirebaseFirestorePlugin) { + this.plugin = plugin; + } - return getProperty; - }, - }); + addDocument(options: AddDocumentOptions): Promise { + return this.plugin.addDocument(formatOptionsData(options)); + } + setDocument(options: SetDocumentOptions): Promise { + return this.plugin.setDocument(formatOptionsData(options)); + } + async getDocument( + options: GetDocumentOptions, + ): Promise> { + return parseResult(await this.plugin.getDocument(options)); + } + updateDocument(options: UpdateDocumentOptions): Promise { + return this.plugin.updateDocument(formatOptionsData(options)); + } + deleteDocument(options: DeleteDocumentOptions): Promise { + return this.plugin.deleteDocument(options); + } + writeBatch(options: WriteBatchOptions): Promise { + return this.plugin.writeBatch({ + ...options, + operations: options.operations.map(operation => + formatOptionsData(operation), + ), + }); + } + async getCollection( + options: GetCollectionOptions, + ): Promise> { + return parseResult(await this.plugin.getCollection(options)); + } + async getCollectionGroup( + options: GetCollectionGroupOptions, + ): Promise> { + return parseResult(await this.plugin.getCollectionGroup(options)); + } + clearPersistence(): Promise { + return this.plugin.clearPersistence(); + } + enableNetwork(): Promise { + return this.plugin.enableNetwork(); + } + disableNetwork(): Promise { + return this.plugin.disableNetwork(); + } + useEmulator(options: UseEmulatorOptions): Promise { + return this.plugin.useEmulator(options); + } + addDocumentSnapshotListener( + options: AddDocumentSnapshotListenerOptions, + callback: AddDocumentSnapshotListenerCallback, + ): Promise { + return this.plugin.addDocumentSnapshotListener(options, (ev, err) => + callback(parseResult(ev), err), + ); + } + addCollectionSnapshotListener( + options: AddCollectionSnapshotListenerOptions, + callback: AddCollectionSnapshotListenerCallback, + ): Promise { + return this.plugin.addCollectionSnapshotListener(options, (ev, err) => + callback(parseResult(ev), err), + ); + } + addCollectionGroupSnapshotListener( + options: AddCollectionGroupSnapshotListenerOptions, + callback: AddCollectionGroupSnapshotListenerCallback, + ): Promise { + return this.plugin.addCollectionGroupSnapshotListener( + options, + (ev, err) => callback(parseResult(ev), err), + ); + } + removeSnapshotListener( + options: RemoveSnapshotListenerOptions, + ): Promise { + return this.plugin.removeSnapshotListener(options); + } + removeAllListeners(): Promise { + return this.plugin.removeAllListeners(); + } } /** @@ -88,23 +137,27 @@ function formatOptionsData(options: T): T { /** * Parse a received result - * @param res The result of a read method + * @param result The result of a read method * @returns The parsed result */ function parseResult< - T, - U extends { - snapshot?: DocumentSnapshot; - snapshots?: DocumentSnapshot[]; - }, ->(res: U): U { - if (res?.snapshot?.data) { - res.snapshot.data = parseResultDocumentData(res.snapshot.data); - } - if (res?.snapshots) { - res.snapshots.map(s => parseResultDocumentData(s)); - } - return res; + T extends DocumentData, + U extends + | Partial> + | Partial> + | null, +>(result: U): U { + if ((result as GetDocumentResult)?.snapshot?.data) { + (result as GetDocumentResult).snapshot.data = parseResultDocumentData( + (result as GetDocumentResult).snapshot.data, + ); + } + if ((result as GetCollectionGroupResult)?.snapshots) { + (result as GetCollectionGroupResult).snapshots.map(s => + parseResultDocumentData(s), + ); + } + return result; } /** diff --git a/packages/firestore/src/index.ts b/packages/firestore/src/index.ts index ce37f58f..ba11d6c7 100644 --- a/packages/firestore/src/index.ts +++ b/packages/firestore/src/index.ts @@ -1,9 +1,9 @@ import { registerPlugin } from '@capacitor/core'; -import { getClientPlugin } from './client'; +import { FirebaseFirestoreClient } from './client'; import type { FirebaseFirestorePlugin } from './definitions'; -const FirebaseFirestore = getClientPlugin( +const FirebaseFirestore: FirebaseFirestorePlugin = new FirebaseFirestoreClient( registerPlugin('FirebaseFirestore', { web: () => import('./web').then(m => new m.FirebaseFirestoreWeb()), }), From 5e4ca01321f18dfec5a23ac12aca2e7d4e787174 Mon Sep 17 00:00:00 2001 From: mamillastre Date: Thu, 9 Jan 2025 14:52:22 +0100 Subject: [PATCH 7/9] rebase & add the getCountFromServer method --- packages/firestore/src/client.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/firestore/src/client.ts b/packages/firestore/src/client.ts index 054a7d26..d83ea748 100644 --- a/packages/firestore/src/client.ts +++ b/packages/firestore/src/client.ts @@ -18,6 +18,8 @@ import type { GetCollectionGroupResult, GetCollectionOptions, GetCollectionResult, + GetCountFromServerOptions, + GetCountFromServerResult, GetDocumentOptions, GetDocumentResult, RemoveSnapshotListenerOptions, @@ -75,6 +77,11 @@ export class FirebaseFirestoreClient implements FirebaseFirestorePlugin { ): Promise> { return parseResult(await this.plugin.getCollectionGroup(options)); } + getCountFromServer( + options: GetCountFromServerOptions, + ): Promise { + return this.plugin.getCountFromServer(options); + } clearPersistence(): Promise { return this.plugin.clearPersistence(); } From 1e696d0c85eeeff5ff8b6e0f83b46ff65162fe3a Mon Sep 17 00:00:00 2001 From: mamillastre Date: Thu, 9 Jan 2025 17:12:59 +0100 Subject: [PATCH 8/9] fix the snapshots parsing --- packages/firestore/src/client.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/firestore/src/client.ts b/packages/firestore/src/client.ts index d83ea748..5d523cb3 100644 --- a/packages/firestore/src/client.ts +++ b/packages/firestore/src/client.ts @@ -160,9 +160,9 @@ function parseResult< ); } if ((result as GetCollectionGroupResult)?.snapshots) { - (result as GetCollectionGroupResult).snapshots.map(s => - parseResultDocumentData(s), - ); + (result as GetCollectionGroupResult).snapshots = ( + result as GetCollectionGroupResult + ).snapshots.map(s => parseResultDocumentData(s)); } return result; } From 36193961b4fb43f9c688d4b04e4a92b9826b8622 Mon Sep 17 00:00:00 2001 From: mamillastre Date: Fri, 10 Jan 2025 17:44:47 +0100 Subject: [PATCH 9/9] add the ios implementation --- .../ios/Plugin.xcodeproj/project.pbxproj | 40 ++++++++++ .../Enums/FirebaseFirestoreError.swift | 3 + .../Classes/Enums/FirestoreFieldType.swift | 6 ++ .../Enums/FirestoreFieldValueMethod.swift | 9 +++ .../Classes/Fields/FirestoreField.swift | 52 +++++++++++++ .../Classes/Fields/FirestoreFieldValue.swift | 37 ++++++++++ .../Classes/Fields/FirestoreTimestamp.swift | 35 +++++++++ .../ios/Plugin/FirebaseFirestoreHelper.swift | 73 +++++++++++++++---- 8 files changed, 239 insertions(+), 16 deletions(-) create mode 100644 packages/firestore/ios/Plugin/Classes/Enums/FirebaseFirestoreError.swift create mode 100644 packages/firestore/ios/Plugin/Classes/Enums/FirestoreFieldType.swift create mode 100644 packages/firestore/ios/Plugin/Classes/Enums/FirestoreFieldValueMethod.swift create mode 100644 packages/firestore/ios/Plugin/Classes/Fields/FirestoreField.swift create mode 100644 packages/firestore/ios/Plugin/Classes/Fields/FirestoreFieldValue.swift create mode 100644 packages/firestore/ios/Plugin/Classes/Fields/FirestoreTimestamp.swift diff --git a/packages/firestore/ios/Plugin.xcodeproj/project.pbxproj b/packages/firestore/ios/Plugin.xcodeproj/project.pbxproj index dc5a56c2..f1b47934 100644 --- a/packages/firestore/ios/Plugin.xcodeproj/project.pbxproj +++ b/packages/firestore/ios/Plugin.xcodeproj/project.pbxproj @@ -45,6 +45,12 @@ 7C90A58E2C21519B00A71389 /* AddCollectionGroupSnapshotListenerOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C90A58D2C21519B00A71389 /* AddCollectionGroupSnapshotListenerOptions.swift */; }; 7CF0EFE32C207844004D910D /* WriteBatchOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CF0EFE22C207844004D910D /* WriteBatchOptions.swift */; }; 7CF0EFE52C20784E004D910D /* WriteBatchOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CF0EFE42C20784E004D910D /* WriteBatchOperation.swift */; }; + DADAB71F2D313A250035799A /* FirestoreFieldValueMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADAB71D2D313A250035799A /* FirestoreFieldValueMethod.swift */; }; + DADAB7202D313A250035799A /* FirestoreFieldType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADAB71C2D313A250035799A /* FirestoreFieldType.swift */; }; + DADAB7252D313A350035799A /* FirestoreTimestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADAB7232D313A350035799A /* FirestoreTimestamp.swift */; }; + DADAB7262D313A350035799A /* FirestoreFieldValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADAB7222D313A350035799A /* FirestoreFieldValue.swift */; }; + DADAB7272D313A350035799A /* FirestoreField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADAB7212D313A350035799A /* FirestoreField.swift */; }; + DADAB7292D3171AF0035799A /* FirebaseFirestoreError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADAB7282D3171990035799A /* FirebaseFirestoreError.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -101,6 +107,12 @@ 7CF0EFE42C20784E004D910D /* WriteBatchOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteBatchOperation.swift; sourceTree = ""; }; 91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Plugin.release.xcconfig"; path = "Pods/Target Support Files/Pods-Plugin/Pods-Plugin.release.xcconfig"; sourceTree = ""; }; 96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PluginTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.debug.xcconfig"; sourceTree = ""; }; + DADAB71C2D313A250035799A /* FirestoreFieldType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirestoreFieldType.swift; sourceTree = ""; }; + DADAB71D2D313A250035799A /* FirestoreFieldValueMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirestoreFieldValueMethod.swift; sourceTree = ""; }; + DADAB7212D313A350035799A /* FirestoreField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirestoreField.swift; sourceTree = ""; }; + DADAB7222D313A350035799A /* FirestoreFieldValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirestoreFieldValue.swift; sourceTree = ""; }; + DADAB7232D313A350035799A /* FirestoreTimestamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirestoreTimestamp.swift; sourceTree = ""; }; + DADAB7282D3171990035799A /* FirebaseFirestoreError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseFirestoreError.swift; sourceTree = ""; }; F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PluginTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.release.xcconfig"; sourceTree = ""; }; F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PluginTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -174,9 +186,11 @@ 7C00700A2ABC1E7F000C0F28 /* Classes */ = { isa = PBXGroup; children = ( + DADAB71E2D313A250035799A /* Enums */, 7C0070382ABC2007000C0F28 /* Results */, 7C0070372ABC1FFC000C0F28 /* Constraints */, 7C0070302ABC1FB6000C0F28 /* Options */, + DADAB7242D313A350035799A /* Fields */, ); path = Classes; sourceTree = ""; @@ -258,6 +272,26 @@ name = Frameworks; sourceTree = ""; }; + DADAB71E2D313A250035799A /* Enums */ = { + isa = PBXGroup; + children = ( + DADAB7282D3171990035799A /* FirebaseFirestoreError.swift */, + DADAB71C2D313A250035799A /* FirestoreFieldType.swift */, + DADAB71D2D313A250035799A /* FirestoreFieldValueMethod.swift */, + ); + path = Enums; + sourceTree = ""; + }; + DADAB7242D313A350035799A /* Fields */ = { + isa = PBXGroup; + children = ( + DADAB7212D313A350035799A /* FirestoreField.swift */, + DADAB7222D313A350035799A /* FirestoreFieldValue.swift */, + DADAB7232D313A350035799A /* FirestoreTimestamp.swift */, + ); + path = Fields; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -462,6 +496,9 @@ buildActionMask = 2147483647; files = ( 7C0070362ABC1FD7000C0F28 /* UpdateDocumentOptions.swift in Sources */, + DADAB71F2D313A250035799A /* FirestoreFieldValueMethod.swift in Sources */, + DADAB7202D313A250035799A /* FirestoreFieldType.swift in Sources */, + DADAB7292D3171AF0035799A /* FirebaseFirestoreError.swift in Sources */, 7C0070322ABC1FC5000C0F28 /* RemoveSnapshotListenerOptions.swift in Sources */, 7C0070242ABC1F53000C0F28 /* QueryCompositeFilterConstraint.swift in Sources */, 7C0070282ABC1F6D000C0F28 /* QueryFieldFilterConstraint.swift in Sources */, @@ -472,6 +509,9 @@ 7C00700E2ABC1ED4000C0F28 /* AddCollectionSnapshotListenerOptions.swift in Sources */, 50E1A94820377CB70090CE1A /* FirebaseFirestorePlugin.swift in Sources */, 7C00702B2ABC1F7B000C0F28 /* QueryLimitConstraint.swift in Sources */, + DADAB7252D313A350035799A /* FirestoreTimestamp.swift in Sources */, + DADAB7262D313A350035799A /* FirestoreFieldValue.swift in Sources */, + DADAB7272D313A350035799A /* FirestoreField.swift in Sources */, 7C0070202ABC1F42000C0F28 /* GetDocumentOptions.swift in Sources */, 7C2AE7CB2D2DC2B400F40418 /* GetCountFromServerResult.swift in Sources */, 7CF0EFE52C20784E004D910D /* WriteBatchOperation.swift in Sources */, diff --git a/packages/firestore/ios/Plugin/Classes/Enums/FirebaseFirestoreError.swift b/packages/firestore/ios/Plugin/Classes/Enums/FirebaseFirestoreError.swift new file mode 100644 index 00000000..ad52e7cf --- /dev/null +++ b/packages/firestore/ios/Plugin/Classes/Enums/FirebaseFirestoreError.swift @@ -0,0 +1,3 @@ +enum FirebaseFirestoreError: Error { + case invalidArgumant(String) +} diff --git a/packages/firestore/ios/Plugin/Classes/Enums/FirestoreFieldType.swift b/packages/firestore/ios/Plugin/Classes/Enums/FirestoreFieldType.swift new file mode 100644 index 00000000..6dd9472c --- /dev/null +++ b/packages/firestore/ios/Plugin/Classes/Enums/FirestoreFieldType.swift @@ -0,0 +1,6 @@ +import Foundation + +enum FirestoreFieldType: String { + case fieldValue = "fieldvalue" + case timestamp = "timestamp" +} diff --git a/packages/firestore/ios/Plugin/Classes/Enums/FirestoreFieldValueMethod.swift b/packages/firestore/ios/Plugin/Classes/Enums/FirestoreFieldValueMethod.swift new file mode 100644 index 00000000..a6e72cda --- /dev/null +++ b/packages/firestore/ios/Plugin/Classes/Enums/FirestoreFieldValueMethod.swift @@ -0,0 +1,9 @@ +import Foundation + +enum FirestoreFieldValueMethod: String { + case arrayRemove = "arrayRemove" + case arrayUnion = "arrayUnion" + case deleteField = "deleteField" + case increment = "increment" + case serverTimestamp = "serverTimestamp" +} diff --git a/packages/firestore/ios/Plugin/Classes/Fields/FirestoreField.swift b/packages/firestore/ios/Plugin/Classes/Fields/FirestoreField.swift new file mode 100644 index 00000000..97d37d0a --- /dev/null +++ b/packages/firestore/ios/Plugin/Classes/Fields/FirestoreField.swift @@ -0,0 +1,52 @@ +import Foundation +import FirebaseFirestore +import Capacitor + +@objc public class FirestoreField: NSObject { + + private static var FIRESTORE_FIELD_TYPE = "_capacitorFirestoreFieldType" + private static var FIRESTORE_FIELD_VALUE = "_capacitorFirestoreFieldValue" + + private var type: FirestoreFieldType + + private var value: JSObject + + private init(type: FirestoreFieldType, value: JSObject) { + self.type = type + self.value = value + } + + public static func isFirestoreField(_ firestoreFieldData: JSObject) -> Bool { + return firestoreFieldData[FIRESTORE_FIELD_TYPE] != nil + } + + public static func fromJSONObject(_ data: JSObject) -> FirestoreField { + return FirestoreField( + type: FirestoreFieldType(rawValue: data[FIRESTORE_FIELD_TYPE]! as! String)!, + value: data[FIRESTORE_FIELD_VALUE]! as! JSObject + ) + } + + public static func fromObject(_ object: Any) throws -> FirestoreField { + if object is Timestamp { + let timestamp = FirestoreTimestamp.fromFirestore(object as! Timestamp) + return FirestoreField(type: .timestamp, value: timestamp.getValue()) + } + throw FirebaseFirestoreError.invalidArgumant("The provided object is not a firestore field") + } + + public func getField() -> Any { + switch type { + case .fieldValue: return FirestoreFieldValue.fromJSONObject(value).getField() + case .timestamp: return FirestoreTimestamp.fromJSONObject(value).getField() + } + } + + public func getJSObject() -> JSObject { + var object: JSObject = [:] + object[FirestoreField.FIRESTORE_FIELD_TYPE] = type.rawValue + object[FirestoreField.FIRESTORE_FIELD_VALUE] = value + return object + } + +} diff --git a/packages/firestore/ios/Plugin/Classes/Fields/FirestoreFieldValue.swift b/packages/firestore/ios/Plugin/Classes/Fields/FirestoreFieldValue.swift new file mode 100644 index 00000000..8c4fff50 --- /dev/null +++ b/packages/firestore/ios/Plugin/Classes/Fields/FirestoreFieldValue.swift @@ -0,0 +1,37 @@ +import Foundation +import FirebaseFirestore +import Capacitor + +@objc public class FirestoreFieldValue: NSObject { + + private var method: FirestoreFieldValueMethod + + private var args: [Any] + + private init(method: FirestoreFieldValueMethod, args: [Any]) { + self.method = method + self.args = args + } + + public static func fromJSONObject(_ data: JSObject) -> FirestoreFieldValue { + return FirestoreFieldValue( + method: FirestoreFieldValueMethod(rawValue: data["method"]! as! String)!, + args: FirebaseFirestoreHelper.createObjectFromJSValue(data["args"]!) as! [Any] + ) + } + + public func getField() -> Any { + switch method { + case .arrayRemove: return FieldValue.arrayRemove(args) + case .arrayUnion: return FieldValue.arrayUnion(args) + case .deleteField: return FieldValue.delete() + case .increment: + if args[0] is Double { + return FieldValue.increment(args[0] as! Double) + } + return FieldValue.increment((args[0] as! NSNumber).int64Value) + case .serverTimestamp: return FieldValue.serverTimestamp() + } + } + +} diff --git a/packages/firestore/ios/Plugin/Classes/Fields/FirestoreTimestamp.swift b/packages/firestore/ios/Plugin/Classes/Fields/FirestoreTimestamp.swift new file mode 100644 index 00000000..e372c879 --- /dev/null +++ b/packages/firestore/ios/Plugin/Classes/Fields/FirestoreTimestamp.swift @@ -0,0 +1,35 @@ +import Foundation +import FirebaseFirestore +import Capacitor + +@objc public class FirestoreTimestamp: NSObject { + + private var seconds: Int64 + + private var nanoseconds: Int32 + + private init(seconds: Int64, nanoseconds: Int32) { + self.seconds = seconds + self.nanoseconds = nanoseconds + } + + public static func fromJSONObject(_ value: JSObject) -> FirestoreTimestamp { + return FirestoreTimestamp(seconds: Int64(value["seconds"] as! Int), nanoseconds: Int32(value["nanoseconds"] as! Int)) + } + + public static func fromFirestore(_ timestamp: Timestamp) -> FirestoreTimestamp { + return FirestoreTimestamp(seconds: timestamp.seconds, nanoseconds: timestamp.nanoseconds) + } + + public func getValue() -> JSObject { + var value: JSObject = [:] + value["seconds"] = NSNumber(value: seconds) + value["nanoseconds"] = NSNumber(value: nanoseconds) + return value + } + + public func getField() -> Timestamp { + return Timestamp(seconds: seconds, nanoseconds: nanoseconds) + } + +} diff --git a/packages/firestore/ios/Plugin/FirebaseFirestoreHelper.swift b/packages/firestore/ios/Plugin/FirebaseFirestoreHelper.swift index cf94c9ab..37cdb52e 100644 --- a/packages/firestore/ios/Plugin/FirebaseFirestoreHelper.swift +++ b/packages/firestore/ios/Plugin/FirebaseFirestoreHelper.swift @@ -4,24 +4,32 @@ import Capacitor public class FirebaseFirestoreHelper { public static func createHashMapFromJSObject(_ object: JSObject) -> [String: Any] { - var map: [String: Any] = [:] - for key in object.keys { - if let value = object[key] { - map[key] = value + return createObjectFromJSValue(object) as! [String: Any] + } + + public static func createObjectFromJSValue(_ value: JSValue) -> Any? { + if let object = value as? JSObject { + if FirestoreField.isFirestoreField(object) { + let field = FirestoreField.fromJSONObject(object) + return field.getField() + } + var map: [String: Any] = [:] + for key in object.keys { + if let v = object[key] { + map[key] = createObjectFromJSValue(v) + } } + return map } - return map + if let array = value as? JSArray { + return array.map { createObjectFromJSValue($0) } + } + + return value } public static func createJSObjectFromHashMap(_ map: [String: Any]?) -> JSObject? { - guard let map = map else { - return nil - } - var object: JSObject = [:] - for key in map.keys { - object[key] = self.createJSValue(value: map[key]) - } - return object + return createJSValue(map) as? JSObject } public static func createQueryCompositeFilterConstraintFromJSObject(_ compositeFilter: JSObject?) -> QueryCompositeFilterConstraint? { @@ -60,13 +68,46 @@ public class FirebaseFirestoreHelper { } } - private static func createJSValue(value: Any?) -> JSValue? { + private static func createJSValue(_ value: Any?) -> JSValue? { guard let value = value else { return nil } - guard let value = JSTypes.coerceDictionaryToJSObject(["key": value]) as JSObject? else { + + switch value { + case let timestampValue as Timestamp: + do { + return try FirestoreField.fromObject(timestampValue).getJSObject() + } catch { + return nil + } + case let stringValue as String: + return stringValue + case let numberValue as NSNumber: + return numberValue + case let boolValue as Bool: + return boolValue + case let intValue as Int: + return intValue + case let floatValue as Float: + return floatValue + case let doubleValue as Double: + return doubleValue + case let dateValue as Date: + return dateValue + case let nullValue as NSNull: + return nullValue + case let arrayValue as NSArray: + return arrayValue.compactMap { createJSValue($0) } + case let dictionaryValue as NSDictionary: + let keys = dictionaryValue.allKeys.compactMap { $0 as? String } + var result: JSObject = [:] + for key in keys { + result[key] = createJSValue(dictionaryValue[key]) + } + return result + default: return nil } - return value["key"] } + }