@shisyamo4131/air-firebase-v2-client-adapter 1.0.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/index.js +996 -0
- package/package.json +31 -0
package/index.js
ADDED
|
@@ -0,0 +1,996 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* アプリ側で使用する FireModel のアダプターです。
|
|
3
|
+
* FireModel に Firestore に対する CRUD 機能を注入します。
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
collection,
|
|
7
|
+
doc,
|
|
8
|
+
getDoc,
|
|
9
|
+
runTransaction,
|
|
10
|
+
query,
|
|
11
|
+
getDocs,
|
|
12
|
+
where,
|
|
13
|
+
orderBy,
|
|
14
|
+
limit,
|
|
15
|
+
collectionGroup,
|
|
16
|
+
onSnapshot,
|
|
17
|
+
getFirestore,
|
|
18
|
+
increment as FieldValue_increment,
|
|
19
|
+
} from "firebase/firestore";
|
|
20
|
+
import { getAuth } from "firebase/auth";
|
|
21
|
+
import { ClientAdapterError, ERRORS } from "./error.js";
|
|
22
|
+
|
|
23
|
+
/*****************************************************************************
|
|
24
|
+
* Client Adapter for FireModel version 1.0.0
|
|
25
|
+
*
|
|
26
|
+
* - This adapter is designed for client-side applications using Firebase.
|
|
27
|
+
*****************************************************************************/
|
|
28
|
+
class ClientAdapter {
|
|
29
|
+
static firestore = null;
|
|
30
|
+
static auth = null;
|
|
31
|
+
|
|
32
|
+
constructor() {
|
|
33
|
+
ClientAdapter.firestore = getFirestore();
|
|
34
|
+
ClientAdapter.auth = getAuth();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get type() {
|
|
38
|
+
return "CLIENT";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* console を返します。
|
|
43
|
+
* FireModel でコンソールを出力するために使用します。
|
|
44
|
+
*/
|
|
45
|
+
get logger() {
|
|
46
|
+
return console;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns the Authentication instance.
|
|
51
|
+
*/
|
|
52
|
+
get auth() {
|
|
53
|
+
if (!ClientAdapter.auth) {
|
|
54
|
+
throw new ClientAdapterError(
|
|
55
|
+
ERRORS.SYSTEM_AUTHENTICATION_NOT_INITIALIZED
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return ClientAdapter.auth;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns the Firestore instance.
|
|
63
|
+
* - 2025-07-11 added
|
|
64
|
+
*/
|
|
65
|
+
get firestore() {
|
|
66
|
+
if (!ClientAdapter.firestore) {
|
|
67
|
+
throw new ClientAdapterError(ERRORS.SYSTEM_FIRESTORE_NOT_INITIALIZED);
|
|
68
|
+
}
|
|
69
|
+
return ClientAdapter.firestore;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Assigns an autonumber to the instance using a Firestore transaction.
|
|
74
|
+
* - Retrieves the current autonumber doc from the `Autonumbers` collection.
|
|
75
|
+
* - Increments the number and sets it on the instance.
|
|
76
|
+
* - Returns a function to update the `current` value in Firestore.
|
|
77
|
+
* - `prefix` is used to resolve the collection path if provided.
|
|
78
|
+
* @param {Object} args - Autonumber options.
|
|
79
|
+
* @param {Object} args.transaction - Firestore transaction object (required).
|
|
80
|
+
* @param {string|null} [args.prefix=null] - Optional path prefix.
|
|
81
|
+
* @returns {Promise<Function>} Function that updates the current counter.
|
|
82
|
+
* @throws {Error} If transaction is not provided or autonumber is invalid.
|
|
83
|
+
*/
|
|
84
|
+
async setAutonumber({ transaction, prefix = null } = {}) {
|
|
85
|
+
if (!transaction) {
|
|
86
|
+
throw new ClientAdapterError(ERRORS.VALIDATION_MISSING_TRANSACTION);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
let effectivePrefix =
|
|
91
|
+
prefix || this.constructor.getConfig()?.prefix || "";
|
|
92
|
+
if (effectivePrefix && !effectivePrefix.endsWith("/")) {
|
|
93
|
+
effectivePrefix += "/";
|
|
94
|
+
}
|
|
95
|
+
const collectionPath = effectivePrefix + "Autonumbers";
|
|
96
|
+
const docRef = doc(collection(ClientAdapter.firestore, collectionPath));
|
|
97
|
+
const docSnap = await transaction.get(docRef);
|
|
98
|
+
if (!docSnap.exists()) {
|
|
99
|
+
throw new ClientAdapterError(
|
|
100
|
+
ERRORS.BUSINESS_AUTONUMBER_DOCUMENT_NOT_FOUND
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const data = docSnap.data();
|
|
105
|
+
|
|
106
|
+
if (!data?.status) {
|
|
107
|
+
throw new ClientAdapterError(ERRORS.BUSINESS_AUTONUMBER_DISABLED);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const newNumber = data.current + 1;
|
|
111
|
+
const length = data.length;
|
|
112
|
+
const maxValue = Math.pow(10, length) - 1;
|
|
113
|
+
if (newNumber > maxValue) {
|
|
114
|
+
throw new ClientAdapterError(ERRORS.BUSINESS_AUTONUMBER_MAX_REACHED);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const newCode = String(newNumber).padStart(length, "0");
|
|
118
|
+
this[data.field] = newCode;
|
|
119
|
+
|
|
120
|
+
return () => transaction.update(docRef, { current: newNumber });
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (err instanceof ClientAdapterError) {
|
|
123
|
+
throw err;
|
|
124
|
+
} else {
|
|
125
|
+
this._outputErrorConsole("setAutonumber", err);
|
|
126
|
+
throw new ClientAdapterError(ERRORS.SYSTEM_UNKNOWN_ERROR);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// /**
|
|
132
|
+
// * Returns a function to update the counter document in Firestore.
|
|
133
|
+
// * - This function treats 'this' as a FireModel instance.
|
|
134
|
+
// * @param {Object} args - Parameters for counter update.
|
|
135
|
+
// * @param {Object} args.transaction - Firestore transaction object (required).
|
|
136
|
+
// * @param {boolean} [args.increment=true] - Whether to increment (true) or decrement (false) the counter.
|
|
137
|
+
// * @param {string|null} [args.prefix=null] - Optional path prefix for collection.
|
|
138
|
+
// * @returns {Promise<Function>} Function to update the counter document.
|
|
139
|
+
// */
|
|
140
|
+
// async getCounterUpdater(args = {}) {
|
|
141
|
+
// const { transaction, increment = true, prefix = null } = args;
|
|
142
|
+
// // transaction is required
|
|
143
|
+
// if (!transaction) {
|
|
144
|
+
// throw new ClientAdapterError(ERRORS.VALIDATION_MISSING_TRANSACTION);
|
|
145
|
+
// }
|
|
146
|
+
|
|
147
|
+
// try {
|
|
148
|
+
// // Get collection path defined by class.
|
|
149
|
+
// // -> `getCollectionPath()` is a static method defined in FireModel.
|
|
150
|
+
// // ex) `customers` or `companies/{companyId}/customers`
|
|
151
|
+
// const collectionPath = this.constructor.getCollectionPath(prefix);
|
|
152
|
+
|
|
153
|
+
// // Divide collection path into segments.
|
|
154
|
+
// // ex) `["companies", "{companyId}", "customers"]`
|
|
155
|
+
// const segments = collectionPath.split("/");
|
|
156
|
+
|
|
157
|
+
// // Get collection name (Last segment is collection name)
|
|
158
|
+
// const colName = segments.pop();
|
|
159
|
+
|
|
160
|
+
// // Determine effective collection path for counter-document.
|
|
161
|
+
// const effectiveDocPath = `${segments.join("/")}/meta/docCounter`;
|
|
162
|
+
// const docRef = doc(ClientAdapter.firestore, effectiveDocPath);
|
|
163
|
+
// const docSnap = await transaction.get(docRef);
|
|
164
|
+
// if (!docSnap.exists()) {
|
|
165
|
+
// return () => transaction.set(docRef, { [colName]: increment ? 1 : 0 });
|
|
166
|
+
// } else {
|
|
167
|
+
// return () =>
|
|
168
|
+
// transaction.update(docRef, {
|
|
169
|
+
// [colName]: FieldValue_increment(increment ? 1 : -1),
|
|
170
|
+
// });
|
|
171
|
+
// }
|
|
172
|
+
// } catch (err) {
|
|
173
|
+
// if (err instanceof ClientAdapterError) {
|
|
174
|
+
// throw err;
|
|
175
|
+
// } else {
|
|
176
|
+
// this._outputErrorConsole("getCounterUpdater", err);
|
|
177
|
+
// throw new ClientAdapterError(ERRORS.SYSTEM_UNKNOWN_ERROR);
|
|
178
|
+
// }
|
|
179
|
+
// }
|
|
180
|
+
// }
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Create a new document in Firestore.
|
|
184
|
+
* @param {Object} args - Creation options.
|
|
185
|
+
* @param {string} [args.docId] - Document ID to use (optional).
|
|
186
|
+
* @param {boolean} [args.useAutonumber=true] - Whether to use auto-numbering.
|
|
187
|
+
* @param {Object} [args.transaction] - Firestore transaction.
|
|
188
|
+
* @param {Function} [args.callback] - Callback function.
|
|
189
|
+
* @param {string} [args.prefix] - Path prefix.
|
|
190
|
+
* @returns {Promise<DocumentReference>} Reference to the created document.
|
|
191
|
+
* @throws {Error} If creation fails or `callback` is not a function.
|
|
192
|
+
*/
|
|
193
|
+
async create(args = {}) {
|
|
194
|
+
const { docId, useAutonumber = true, transaction, callback, prefix } = args;
|
|
195
|
+
|
|
196
|
+
// `callback` must be a function if provided.
|
|
197
|
+
if (callback && typeof callback !== "function") {
|
|
198
|
+
throw new ClientAdapterError(ERRORS.VALIDATION_INVALID_CALLBACK);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
// Pre-create hooks and validation
|
|
203
|
+
await this.beforeCreate();
|
|
204
|
+
await this.beforeEdit();
|
|
205
|
+
this.validate();
|
|
206
|
+
|
|
207
|
+
// transaction processing
|
|
208
|
+
const performTransaction = async (txn) => {
|
|
209
|
+
// Get function to update autonumber if `useAutonumber` is true.
|
|
210
|
+
const updateAutonumber =
|
|
211
|
+
this.constructor.useAutonumber && useAutonumber
|
|
212
|
+
? await this.setAutonumber({ transaction: txn, prefix })
|
|
213
|
+
: null;
|
|
214
|
+
|
|
215
|
+
// Get function to update counter document.
|
|
216
|
+
// const adapter = this.constructor.getAdapter();
|
|
217
|
+
// const counterUpdater = await adapter.getCounterUpdater.bind(this)({
|
|
218
|
+
// transaction: txn,
|
|
219
|
+
// increment: true,
|
|
220
|
+
// prefix,
|
|
221
|
+
// });
|
|
222
|
+
|
|
223
|
+
// Prepare document reference
|
|
224
|
+
const collectionPath = this.constructor.getCollectionPath(prefix);
|
|
225
|
+
const colRef = collection(
|
|
226
|
+
ClientAdapter.firestore,
|
|
227
|
+
collectionPath
|
|
228
|
+
).withConverter(this.constructor.converter());
|
|
229
|
+
const docRef = docId ? doc(colRef, docId) : doc(colRef);
|
|
230
|
+
|
|
231
|
+
// Set metadata
|
|
232
|
+
this.docId = docRef.id;
|
|
233
|
+
this.createdAt = new Date();
|
|
234
|
+
this.updatedAt = new Date();
|
|
235
|
+
this.uid = ClientAdapter.auth?.currentUser?.uid || "unknown";
|
|
236
|
+
|
|
237
|
+
// Create document
|
|
238
|
+
txn.set(docRef, this);
|
|
239
|
+
|
|
240
|
+
// Update autonumber if applicable
|
|
241
|
+
if (updateAutonumber) await updateAutonumber();
|
|
242
|
+
|
|
243
|
+
// // Update counter document
|
|
244
|
+
// if (counterUpdater) await counterUpdater();
|
|
245
|
+
|
|
246
|
+
// Execute callback if provided
|
|
247
|
+
if (callback) await callback(txn);
|
|
248
|
+
|
|
249
|
+
// Return document reference
|
|
250
|
+
return docRef;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const docRef = transaction
|
|
254
|
+
? await performTransaction(transaction)
|
|
255
|
+
: await runTransaction(ClientAdapter.firestore, performTransaction);
|
|
256
|
+
|
|
257
|
+
return docRef;
|
|
258
|
+
} catch (err) {
|
|
259
|
+
if (err instanceof ClientAdapterError) {
|
|
260
|
+
throw err;
|
|
261
|
+
} else {
|
|
262
|
+
this._outputErrorConsole("create", err);
|
|
263
|
+
throw new ClientAdapterError(ERRORS.SYSTEM_UNKNOWN_ERROR);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get a document from Firestore by its ID and load into this instance.
|
|
270
|
+
* - The class properties will be cleared if the document does not exist.
|
|
271
|
+
* @param {Object} args - Fetch options.
|
|
272
|
+
* @param {string} args.docId - Document ID to fetch.
|
|
273
|
+
* @param {Object|null} [args.transaction=null] - Firestore transaction (optional).
|
|
274
|
+
* @param {string|null} [args.prefix=null] - Path prefix (optional).
|
|
275
|
+
* @returns {Promise<boolean>} True if document was found and loaded, false if not found.
|
|
276
|
+
* @throws {Error} If `docId` is not specified or fetch fails.
|
|
277
|
+
*/
|
|
278
|
+
async fetch(args = {}) {
|
|
279
|
+
const { docId, transaction = null, prefix = null } = args;
|
|
280
|
+
if (!docId) {
|
|
281
|
+
throw new ClientAdapterError(ERRORS.VALIDATION_MISSING_DOC_ID);
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
// Get collection path defined by FireModel.
|
|
285
|
+
const collectionPath = this.constructor.getCollectionPath(prefix);
|
|
286
|
+
|
|
287
|
+
// Prepare document reference.
|
|
288
|
+
const colRef = collection(
|
|
289
|
+
ClientAdapter.firestore,
|
|
290
|
+
collectionPath
|
|
291
|
+
).withConverter(this.constructor.converter());
|
|
292
|
+
const docRef = doc(colRef, docId);
|
|
293
|
+
|
|
294
|
+
// Fetch document snapshot.
|
|
295
|
+
const docSnap = transaction
|
|
296
|
+
? await transaction.get(docRef)
|
|
297
|
+
: await getDoc(docRef);
|
|
298
|
+
|
|
299
|
+
// Load data into this instance, or reset if not found.
|
|
300
|
+
this.initialize(docSnap.exists() ? docSnap.data() : null);
|
|
301
|
+
|
|
302
|
+
return docSnap.exists();
|
|
303
|
+
} catch (err) {
|
|
304
|
+
if (err instanceof ClientAdapterError) {
|
|
305
|
+
throw err;
|
|
306
|
+
} else {
|
|
307
|
+
this._outputErrorConsole("fetch", err);
|
|
308
|
+
throw new ClientAdapterError(ERRORS.SYSTEM_UNKNOWN_ERROR);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Get a document from Firestore by its ID and return as a new instance.
|
|
315
|
+
* @param {Object} args - Fetch options.
|
|
316
|
+
* @param {string} args.docId - Document ID to fetch.
|
|
317
|
+
* @param {Object|null} [args.transaction=null] - Firestore transaction (optional).
|
|
318
|
+
* @param {string|null} [args.prefix=null] - Path prefix (optional).
|
|
319
|
+
* @returns {Promise<Object|null>} Document data, or null if not found.
|
|
320
|
+
* @throws {Error} If `docId` is not specified or fetch fails.
|
|
321
|
+
*/
|
|
322
|
+
async fetchDoc(args = {}) {
|
|
323
|
+
const { docId, transaction = null, prefix = null } = args;
|
|
324
|
+
// Throw error if docId is not provided.
|
|
325
|
+
if (!docId) {
|
|
326
|
+
throw new ClientAdapterError(ERRORS.VALIDATION_MISSING_DOC_ID);
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
// Get collection path defined by FireModel.
|
|
330
|
+
const collectionPath = this.constructor.getCollectionPath(prefix);
|
|
331
|
+
|
|
332
|
+
// Prepare document reference.
|
|
333
|
+
const colRef = collection(
|
|
334
|
+
ClientAdapter.firestore,
|
|
335
|
+
collectionPath
|
|
336
|
+
).withConverter(this.constructor.converter());
|
|
337
|
+
const docRef = doc(colRef, docId);
|
|
338
|
+
|
|
339
|
+
// Fetch document snapshot.
|
|
340
|
+
const docSnap = transaction
|
|
341
|
+
? await transaction.get(docRef)
|
|
342
|
+
: await getDoc(docRef);
|
|
343
|
+
|
|
344
|
+
return docSnap.exists() ? docSnap.data() : null;
|
|
345
|
+
} catch (err) {
|
|
346
|
+
if (err instanceof ClientAdapterError) {
|
|
347
|
+
throw err;
|
|
348
|
+
} else {
|
|
349
|
+
this._outputErrorConsole("fetchDoc", err);
|
|
350
|
+
throw new ClientAdapterError(ERRORS.SYSTEM_UNKNOWN_ERROR);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Firestore のクエリ条件の配列を受け取り、Firestore のクエリオブジェクト配列を生成して返します。
|
|
357
|
+
* - `constraints` 配列には、`where`, `orderBy`, `limit` などの Firestore クエリを指定できます。
|
|
358
|
+
* - 例:`[['where', 'age', '>=', 18], ['orderBy', 'age', 'desc'], ['limit', 10]]`
|
|
359
|
+
* - 不明なクエリタイプが指定された場合はエラーをスローします。
|
|
360
|
+
*
|
|
361
|
+
* @param {Array} constraints - クエリ条件の配列です。
|
|
362
|
+
* @returns {Array<Object>} - Firestore クエリオブジェクトの配列を返します。
|
|
363
|
+
* @throws {Error} - 不明なクエリタイプが指定された場合、エラーをスローします。
|
|
364
|
+
*/
|
|
365
|
+
createQueries(constraints) {
|
|
366
|
+
const result = [];
|
|
367
|
+
constraints.forEach((constraint) => {
|
|
368
|
+
const [type, ...args] = constraint;
|
|
369
|
+
|
|
370
|
+
switch (type) {
|
|
371
|
+
case "where":
|
|
372
|
+
result.push(where(...args));
|
|
373
|
+
break;
|
|
374
|
+
case "orderBy":
|
|
375
|
+
if (!["asc", "desc"].includes(args[1] || "asc")) {
|
|
376
|
+
throw new ClientAdapterError(
|
|
377
|
+
ERRORS.VALIDATION_INVALID_ORDERBY_DIRECTION
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
result.push(orderBy(args[0], args[1] || "asc"));
|
|
381
|
+
break;
|
|
382
|
+
case "limit":
|
|
383
|
+
if (typeof args[0] !== "number" || args[0] <= 0) {
|
|
384
|
+
throw new ClientAdapterError(ERRORS.VALIDATION_INVALID_LIMIT);
|
|
385
|
+
}
|
|
386
|
+
result.push(limit(args[0]));
|
|
387
|
+
break;
|
|
388
|
+
default:
|
|
389
|
+
throw new ClientAdapterError(ERRORS.VALIDATION_INVALID_QUERY_TYPE);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Firestore の `tokenMap` に基づく N-Gram 検索用のクエリオブジェクトを生成します。
|
|
397
|
+
* - 検索文字列の 1 文字・2 文字ごとのトークンを作成し、Firestore の `tokenMap` を利用した検索クエリを生成します。
|
|
398
|
+
* - 例:`"検索"` → `['検', '索', '検索']`
|
|
399
|
+
* - サロゲートペア文字(絵文字など)は Firestore の `tokenMap` では検索対象としないため除外します。
|
|
400
|
+
*
|
|
401
|
+
* @param {string} constraints - 検索に使用する文字列です。
|
|
402
|
+
* @returns {Array<Object>} - Firestore クエリオブジェクトの配列を返します。
|
|
403
|
+
* @throws {Error} - `constraints` が空文字の場合、エラーをスローします。
|
|
404
|
+
*/
|
|
405
|
+
createTokenMapQueries(constraints) {
|
|
406
|
+
if (!constraints || constraints.trim().length === 0) {
|
|
407
|
+
throw new ClientAdapterError(ERRORS.VALIDATION_INVALID_CONSTRAINTS);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const result = new Set(); // クエリの重複を防ぐために `Set` を使用
|
|
411
|
+
|
|
412
|
+
// サロゲートペア文字(絵文字など)を除外
|
|
413
|
+
const target = constraints.replace(
|
|
414
|
+
/[\uD800-\uDBFF]|[\uDC00-\uDFFF]|~|\*|\[|\]|\s+/g,
|
|
415
|
+
""
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
// 1 文字・2 文字のトークンを生成
|
|
419
|
+
const tokens = [
|
|
420
|
+
...new Set([
|
|
421
|
+
...[...target].map((_, i) => target.substring(i, i + 1)), // 1 文字トークン
|
|
422
|
+
...[...target].map((_, i) => target.substring(i, i + 2)).slice(0, -1), // 2 文字トークン
|
|
423
|
+
]),
|
|
424
|
+
];
|
|
425
|
+
|
|
426
|
+
// Firestore クエリオブジェクトを作成
|
|
427
|
+
tokens.forEach((token) => {
|
|
428
|
+
result.add(where(`tokenMap.${token}`, "==", true));
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
return Array.from(result); // `Set` を配列に変換して返す
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* クエリ条件に一致するドキュメントを Firestore から取得します。
|
|
436
|
+
* - `constraints` が文字列なら N-gram 検索を実行します。
|
|
437
|
+
* - 配列なら通常のクエリ検索を行います。
|
|
438
|
+
* - `prefix` が指定されている場合は、コレクションパスの解決に使用されます。
|
|
439
|
+
*
|
|
440
|
+
* [NOTE]
|
|
441
|
+
* - 2025/10/06 現在、transaction.get() に Query を指定することはできない仕様。
|
|
442
|
+
* そのため、依存ドキュメントの存在確認には getDocs() を使用することになるが、
|
|
443
|
+
* transaction 内での読み取りにならず、当該処理の直後に他のプロセスから依存ドキュメントが
|
|
444
|
+
* 追加された場合に整合性を失う可能性あり。
|
|
445
|
+
* 引数 transaction が本来であれば不要だが、将来的に transaction.get() が
|
|
446
|
+
* Query に対応した場合に備えて引数として受け取る形にしておく。
|
|
447
|
+
*
|
|
448
|
+
* @param {Object} args - Fetch options.
|
|
449
|
+
* @param {Array|string} args.constraints - Query condition array or search string.
|
|
450
|
+
* @param {Array} [args.options=[]] - Additional query filters (ignored if constraints is an array).
|
|
451
|
+
* @param {Object|null} [args.transaction=null] - Firestore transaction (optional).
|
|
452
|
+
* @param {string|null} [args.prefix=null] - Optional Firestore path prefix.
|
|
453
|
+
* @returns {Promise<Array<Object>>} Array of document data.
|
|
454
|
+
* @throws {Error} If constraints are invalid or Firestore query fails.
|
|
455
|
+
*/
|
|
456
|
+
async fetchDocs({
|
|
457
|
+
constraints = [],
|
|
458
|
+
options = [],
|
|
459
|
+
transaction = null,
|
|
460
|
+
prefix = null,
|
|
461
|
+
} = {}) {
|
|
462
|
+
const queryConstraints = [];
|
|
463
|
+
|
|
464
|
+
if (typeof constraints === "string") {
|
|
465
|
+
queryConstraints.push(...this.createTokenMapQueries(constraints));
|
|
466
|
+
queryConstraints.push(...this.createQueries(options));
|
|
467
|
+
} else if (Array.isArray(constraints)) {
|
|
468
|
+
queryConstraints.push(...this.createQueries(constraints));
|
|
469
|
+
} else {
|
|
470
|
+
throw new ClientAdapterError(ERRORS.VALIDATION_INVALID_CONSTRAINTS);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
const collectionPath = this.constructor.getCollectionPath(prefix);
|
|
475
|
+
const colRef = collection(
|
|
476
|
+
ClientAdapter.firestore,
|
|
477
|
+
collectionPath
|
|
478
|
+
).withConverter(this.constructor.converter());
|
|
479
|
+
|
|
480
|
+
const queryRef = query(colRef, ...queryConstraints);
|
|
481
|
+
|
|
482
|
+
/** transaction.get() が Query に対応した場合は以下をコメントアウト */
|
|
483
|
+
const querySnapshot = await getDocs(queryRef);
|
|
484
|
+
|
|
485
|
+
/** transaction.get() が Query に対応した場合は以下を使用 */
|
|
486
|
+
// const querySnapshot = transaction
|
|
487
|
+
// ? await transaction.get(queryRef)
|
|
488
|
+
// : await getDocs(queryRef);
|
|
489
|
+
|
|
490
|
+
return querySnapshot.docs.map((doc) => doc.data());
|
|
491
|
+
} catch (err) {
|
|
492
|
+
if (err instanceof ClientAdapterError) {
|
|
493
|
+
throw err;
|
|
494
|
+
} else {
|
|
495
|
+
this._outputErrorConsole("fetchDocs", err);
|
|
496
|
+
throw new ClientAdapterError(ERRORS.SYSTEM_UNKNOWN_ERROR);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* 指定されたドキュメント ID の配列に該当するドキュメントを取得して返します。
|
|
503
|
+
* - `prefix` が指定されている場合は、コレクションパスの解決に使用されます。
|
|
504
|
+
*
|
|
505
|
+
* [NOTE]
|
|
506
|
+
* - 2025/10/06 現在、transaction.get() に Query を指定することはできない仕様。
|
|
507
|
+
* そのため、依存ドキュメントの存在確認には getDocs() を使用することになるが、
|
|
508
|
+
* transaction 内での読み取りにならず、当該処理の直後に他のプロセスから依存ドキュメントが
|
|
509
|
+
* 追加された場合に整合性を失う可能性あり。
|
|
510
|
+
* 引数 transaction が本来であれば不要だが、将来的に transaction.get() が
|
|
511
|
+
* Query に対応した場合に備えて引数として受け取る形にしておく。
|
|
512
|
+
*
|
|
513
|
+
* @param {Object} args - Fetch options.
|
|
514
|
+
* @param {Array<string>} args.ids - Document ID の配列。
|
|
515
|
+
* @param {Object|null} [args.transaction=null] - Firestore transaction (optional).
|
|
516
|
+
* @param {string|null} [args.prefix=null] - Optional Firestore path prefix.
|
|
517
|
+
* @returns {Promise<Array<Object>>} Array of document data.
|
|
518
|
+
*/
|
|
519
|
+
async fetchDocsByIds({ ids = [], transaction = null, prefix = null } = {}) {
|
|
520
|
+
try {
|
|
521
|
+
if (!Array.isArray(ids) || ids.length === 0) return [];
|
|
522
|
+
|
|
523
|
+
const uniqueIds = Array.from(new Set(ids));
|
|
524
|
+
const chunkedIds = uniqueIds.flatMap((_, i, a) => {
|
|
525
|
+
return i % 30 ? [] : [a.slice(i, i + 30)];
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
const collectionPath = this.constructor.getCollectionPath(prefix);
|
|
529
|
+
const colRef = collection(
|
|
530
|
+
ClientAdapter.firestore,
|
|
531
|
+
collectionPath
|
|
532
|
+
).withConverter(this.constructor.converter());
|
|
533
|
+
|
|
534
|
+
const querySnapshotArray = await Promise.all(
|
|
535
|
+
chunkedIds.map((chunkedId) => {
|
|
536
|
+
const q = query(colRef, where("docId", "in", chunkedId));
|
|
537
|
+
return getDocs(q);
|
|
538
|
+
/** transaction.get() が Query に対応した場合は以下を使用 */
|
|
539
|
+
// return transaction ? transaction.get(q) : getDocs(q);
|
|
540
|
+
})
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
return querySnapshotArray.flatMap((snapshot) =>
|
|
544
|
+
snapshot.docs.map((doc) => doc.data())
|
|
545
|
+
);
|
|
546
|
+
} catch (err) {
|
|
547
|
+
if (err instanceof ClientAdapterError) {
|
|
548
|
+
throw err;
|
|
549
|
+
} else {
|
|
550
|
+
this._outputErrorConsole("fetchDocsByIds", err);
|
|
551
|
+
throw new ClientAdapterError(ERRORS.SYSTEM_UNKNOWN_ERROR);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Updates the Firestore document using the current instance data.
|
|
558
|
+
* - Requires `this.docId` to be set (must call `fetch()` beforehand).
|
|
559
|
+
* - Runs inside a transaction. If not provided, a new one will be created.
|
|
560
|
+
* - If `callback` is specified, it will be executed after the update.
|
|
561
|
+
* - If `prefix` is provided, it is used to resolve the collection path.
|
|
562
|
+
*
|
|
563
|
+
* Firestore ドキュメントを現在のプロパティ値で更新します。
|
|
564
|
+
* - `this.docId` が設定されていない場合はエラーになります(事前に `fetch()` を実行してください)。
|
|
565
|
+
* - 更新はトランザクション内で行われます。トランザクションが指定されない場合は新たに生成されます。
|
|
566
|
+
* - `callback` が指定されていれば、更新後に実行されます。
|
|
567
|
+
* - `prefix` が指定されている場合は、コレクションパスの解決に使用されます。
|
|
568
|
+
*
|
|
569
|
+
* @param {Object} args - Parameters for update operation.
|
|
570
|
+
* 更新処理のためのパラメータ。
|
|
571
|
+
* @param {Object|null} [args.transaction=null] - Firestore transaction object.
|
|
572
|
+
* Firestore のトランザクションオブジェクト。
|
|
573
|
+
* @param {function|null} [args.callback=null] - Callback executed after update.
|
|
574
|
+
* 更新後に実行されるコールバック関数。
|
|
575
|
+
* @param {string|null} [args.prefix=null] - Optional Firestore path prefix.
|
|
576
|
+
* コレクションパスのプレフィックス(任意)。
|
|
577
|
+
* @returns {Promise<DocumentReference>} Reference to the updated document.
|
|
578
|
+
* 更新されたドキュメントのリファレンス。
|
|
579
|
+
* @throws {Error} If `docId` is not set, or if `callback` is not a function.
|
|
580
|
+
* `docId` が未設定、または `callback` が関数でない場合にスローされます。
|
|
581
|
+
*/
|
|
582
|
+
async update({ transaction = null, callback = null, prefix = null } = {}) {
|
|
583
|
+
if (callback !== null && typeof callback !== "function") {
|
|
584
|
+
throw new ClientAdapterError(ERRORS.VALIDATION_INVALID_CALLBACK);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (!this.docId) {
|
|
588
|
+
throw new ClientAdapterError(ERRORS.VALIDATION_MISSING_DOC_ID);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
await this.beforeUpdate();
|
|
593
|
+
await this.beforeEdit();
|
|
594
|
+
this.validate();
|
|
595
|
+
|
|
596
|
+
const performTransaction = async (txn) => {
|
|
597
|
+
const collectionPath = this.constructor.getCollectionPath(prefix);
|
|
598
|
+
const colRef = collection(
|
|
599
|
+
ClientAdapter.firestore,
|
|
600
|
+
collectionPath
|
|
601
|
+
).withConverter(this.constructor.converter());
|
|
602
|
+
const docRef = doc(colRef, this.docId);
|
|
603
|
+
|
|
604
|
+
this.updatedAt = new Date();
|
|
605
|
+
this.uid = ClientAdapter.auth?.currentUser?.uid || "unknown";
|
|
606
|
+
|
|
607
|
+
txn.set(docRef, this);
|
|
608
|
+
if (callback) await callback(txn);
|
|
609
|
+
return docRef;
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
const docRef = transaction
|
|
613
|
+
? await performTransaction(transaction)
|
|
614
|
+
: await runTransaction(ClientAdapter.firestore, performTransaction);
|
|
615
|
+
|
|
616
|
+
return docRef;
|
|
617
|
+
} catch (err) {
|
|
618
|
+
if (err instanceof ClientAdapterError) {
|
|
619
|
+
throw err;
|
|
620
|
+
} else {
|
|
621
|
+
this._outputErrorConsole("update", err);
|
|
622
|
+
throw new ClientAdapterError(ERRORS.SYSTEM_UNKNOWN_ERROR);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Checks if any child documents exist for this document, based on `hasMany` configuration.
|
|
629
|
+
* - For collections, the prefix is applied to the collection path.
|
|
630
|
+
*
|
|
631
|
+
* [NOTE]
|
|
632
|
+
* - 2025/10/06 現在、transaction.get() に Query を指定することはできない仕様。
|
|
633
|
+
* そのため、依存ドキュメントの存在確認には getDocs() を使用することになるが、
|
|
634
|
+
* transaction 内での読み取りにならず、当該処理の直後に他のプロセスから依存ドキュメントが
|
|
635
|
+
* 追加された場合に整合性を失う可能性あり。
|
|
636
|
+
* 引数 transaction が本来であれば不要だが、将来的に transaction.get() が
|
|
637
|
+
* Query に対応した場合に備えて引数として受け取る形にしておく。
|
|
638
|
+
*
|
|
639
|
+
* @param {Object} args - Options for the check.
|
|
640
|
+
* @param {Object|null} [args.transaction=null] - Firestore transaction object (optional).
|
|
641
|
+
* @param {string|null} [args.prefix=null] - Optional path prefix for resolving collections.
|
|
642
|
+
* @returns {Promise<object|boolean>} Matching `hasMany` item if found, otherwise false.
|
|
643
|
+
* @throws {Error} If `docId` is not set or query fails.
|
|
644
|
+
*/
|
|
645
|
+
async hasChild({ transaction = null, prefix = null } = {}) {
|
|
646
|
+
try {
|
|
647
|
+
if (!this.docId) {
|
|
648
|
+
throw new ClientAdapterError(ERRORS.VALIDATION_MISSING_DOC_ID);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
for (const item of this.constructor.hasMany) {
|
|
652
|
+
const collectionPath =
|
|
653
|
+
item.type === "collection" && prefix
|
|
654
|
+
? `${prefix}${item.collectionPath}`
|
|
655
|
+
: item.collectionPath;
|
|
656
|
+
const colRef =
|
|
657
|
+
item.type === "collection"
|
|
658
|
+
? collection(ClientAdapter.firestore, collectionPath)
|
|
659
|
+
: collectionGroup(ClientAdapter.firestore, item.collectionPath);
|
|
660
|
+
const constraint = where(item.field, item.condition, this.docId);
|
|
661
|
+
const queryRef = query(colRef, constraint, limit(1));
|
|
662
|
+
|
|
663
|
+
/** transaction.get() が Query に対応した場合は以下をコメントアウト */
|
|
664
|
+
const snapshot = await getDocs(queryRef);
|
|
665
|
+
|
|
666
|
+
/** transaction.get() が Query に対応した場合は以下を使用 */
|
|
667
|
+
// const snapshot = transaction
|
|
668
|
+
// ? await transaction.get(queryRef)
|
|
669
|
+
// : await getDocs(queryRef);
|
|
670
|
+
|
|
671
|
+
if (!snapshot.empty) return item;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return false;
|
|
675
|
+
} catch (err) {
|
|
676
|
+
if (err instanceof ClientAdapterError) {
|
|
677
|
+
throw err;
|
|
678
|
+
} else {
|
|
679
|
+
this._outputErrorConsole("hasChild", err);
|
|
680
|
+
throw new ClientAdapterError(ERRORS.SYSTEM_UNKNOWN_ERROR);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Deletes the document corresponding to the current `docId`.
|
|
687
|
+
* - If `logicalDelete` is enabled, the document is moved to an archive collection instead of being permanently deleted.
|
|
688
|
+
* - If `transaction` is provided, the deletion is executed within it.
|
|
689
|
+
* - If `prefix` is provided, it will be used to resolve the collection path.
|
|
690
|
+
*
|
|
691
|
+
* 現在の `docId` に該当するドキュメントを削除します。
|
|
692
|
+
* - `logicalDelete` が true の場合、ドキュメントは物理削除されず、アーカイブコレクションに移動されます。
|
|
693
|
+
* - `transaction` が指定されている場合、その中で処理が実行されます。
|
|
694
|
+
* - `prefix` が指定されている場合、それを使ってコレクションパスを解決します。
|
|
695
|
+
*
|
|
696
|
+
* @param {Object} args - Parameters for deletion.
|
|
697
|
+
* 削除処理のパラメータ。
|
|
698
|
+
* @param {Object|null} [args.transaction=null] - Firestore transaction object (optional).
|
|
699
|
+
* Firestore のトランザクションオブジェクト(任意)。
|
|
700
|
+
* @param {function|null} [args.callback=null] - Callback executed after deletion (optional).
|
|
701
|
+
* 削除後に実行されるコールバック関数(任意)。
|
|
702
|
+
* @param {string|null} [args.prefix=null] - Optional Firestore path prefix.
|
|
703
|
+
* コレクションパスのプレフィックス(任意)。
|
|
704
|
+
* @returns {Promise<void>} Resolves when deletion is complete.
|
|
705
|
+
* 削除が完了したら解決されるプロミス。
|
|
706
|
+
* @throws {Error} If `docId` is missing, `callback` is not a function, or document is undeletable.
|
|
707
|
+
* `docId` が未設定、`callback` が関数でない、または削除対象のドキュメントが存在しない場合。
|
|
708
|
+
*/
|
|
709
|
+
async delete({ transaction = null, callback = null, prefix = null } = {}) {
|
|
710
|
+
if (callback !== null && typeof callback !== "function") {
|
|
711
|
+
throw new ClientAdapterError(ERRORS.VALIDATION_INVALID_CALLBACK);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (!this.docId) {
|
|
715
|
+
throw new ClientAdapterError(ERRORS.VALIDATION_MISSING_DOC_ID);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
try {
|
|
719
|
+
await this.beforeDelete();
|
|
720
|
+
|
|
721
|
+
const collectionPath = this.constructor.getCollectionPath(prefix);
|
|
722
|
+
const colRef = collection(ClientAdapter.firestore, collectionPath);
|
|
723
|
+
const docRef = doc(colRef, this.docId);
|
|
724
|
+
|
|
725
|
+
const performTransaction = async (txn) => {
|
|
726
|
+
// Check for child documents before deletion
|
|
727
|
+
// If child documents exist, throw an error to prevent deletion
|
|
728
|
+
const hasChild = await this.hasChild({
|
|
729
|
+
transaction: txn,
|
|
730
|
+
prefix: prefix || this.constructor?.config?.prefix,
|
|
731
|
+
});
|
|
732
|
+
if (hasChild) {
|
|
733
|
+
throw new ClientAdapterError(ERRORS.BUSINESS_CHILD_DOCUMENTS_EXIST);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Get function to update counter document.
|
|
737
|
+
// const adapter = this.constructor.getAdapter();
|
|
738
|
+
// const counterUpdater = await adapter.getCounterUpdater.bind(this)({
|
|
739
|
+
// transaction: txn,
|
|
740
|
+
// increment: false,
|
|
741
|
+
// prefix,
|
|
742
|
+
// });
|
|
743
|
+
|
|
744
|
+
// If logicalDelete is enabled, archive the document before deletion
|
|
745
|
+
if (this.constructor.logicalDelete) {
|
|
746
|
+
// Fetch the document to be deleted
|
|
747
|
+
// This is necessary because in a transaction, docRef.get() cannot be used directly
|
|
748
|
+
// and we need to ensure the document exists before archiving
|
|
749
|
+
const sourceDocSnap = await txn.get(docRef);
|
|
750
|
+
if (!sourceDocSnap.exists()) {
|
|
751
|
+
throw new ClientAdapterError(ERRORS.DATABASE_DOCUMENT_NOT_FOUND);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const sourceDocData = sourceDocSnap.data();
|
|
755
|
+
const archiveColRef = collection(
|
|
756
|
+
ClientAdapter.firestore,
|
|
757
|
+
`${collectionPath}_archive`
|
|
758
|
+
);
|
|
759
|
+
const archiveDocRef = doc(archiveColRef, this.docId);
|
|
760
|
+
txn.set(archiveDocRef, sourceDocData);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
txn.delete(docRef);
|
|
764
|
+
|
|
765
|
+
// if (counterUpdater) await counterUpdater();
|
|
766
|
+
|
|
767
|
+
if (callback) await callback(txn);
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
if (transaction) {
|
|
771
|
+
await performTransaction(transaction);
|
|
772
|
+
} else {
|
|
773
|
+
await runTransaction(ClientAdapter.firestore, performTransaction);
|
|
774
|
+
}
|
|
775
|
+
} catch (err) {
|
|
776
|
+
if (err instanceof ClientAdapterError) {
|
|
777
|
+
throw err;
|
|
778
|
+
} else {
|
|
779
|
+
this._outputErrorConsole("delete", err);
|
|
780
|
+
throw new ClientAdapterError(ERRORS.SYSTEM_UNKNOWN_ERROR);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Restores a deleted document from the archive collection to the original collection.
|
|
787
|
+
* - Uses `prefix` to resolve the Firestore collection path.
|
|
788
|
+
*
|
|
789
|
+
* アーカイブコレクションから削除されたドキュメントを元のコレクションに復元します。
|
|
790
|
+
* - `prefix` が指定されていれば、それに基づいてコレクションパスを解決します。
|
|
791
|
+
*
|
|
792
|
+
* @param {Object} args - Restore options.
|
|
793
|
+
* @param {string} args.docId - Document ID to restore.
|
|
794
|
+
* @param {string|null} [args.prefix=null] - Optional path prefix.
|
|
795
|
+
* @returns {Promise<DocumentReference>} Reference to the restored document.
|
|
796
|
+
* @throws {Error} If document is not found in the archive.
|
|
797
|
+
*/
|
|
798
|
+
async restore({ docId, prefix = null, transaction = null } = {}) {
|
|
799
|
+
if (!docId) {
|
|
800
|
+
throw new ClientAdapterError(ERRORS.VALIDATION_MISSING_DOC_ID);
|
|
801
|
+
}
|
|
802
|
+
try {
|
|
803
|
+
const performTransaction = async (txn) => {
|
|
804
|
+
const collectionPath = this.constructor.getCollectionPath(prefix);
|
|
805
|
+
const archivePath = `${collectionPath}_archive`;
|
|
806
|
+
const archiveColRef = collection(ClientAdapter.firestore, archivePath);
|
|
807
|
+
const archiveDocRef = doc(archiveColRef, docId);
|
|
808
|
+
const docSnapshot = await txn.get(archiveDocRef);
|
|
809
|
+
if (!docSnapshot.exists()) {
|
|
810
|
+
throw new ClientAdapterError(ERRORS.DATABASE_DOCUMENT_NOT_FOUND);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Get function to update counter document.
|
|
814
|
+
// const adapter = this.constructor.getAdapter();
|
|
815
|
+
// const counterUpdater = await adapter.getCounterUpdater.bind(this)({
|
|
816
|
+
// transaction: txn,
|
|
817
|
+
// increment: true,
|
|
818
|
+
// prefix,
|
|
819
|
+
// });
|
|
820
|
+
|
|
821
|
+
const colRef = collection(ClientAdapter.firestore, collectionPath);
|
|
822
|
+
const docRef = doc(colRef, docId);
|
|
823
|
+
txn.delete(archiveDocRef);
|
|
824
|
+
txn.set(docRef, docSnapshot.data());
|
|
825
|
+
|
|
826
|
+
// if (counterUpdater) await counterUpdater();
|
|
827
|
+
|
|
828
|
+
return docRef;
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
if (transaction) {
|
|
832
|
+
return await performTransaction(transaction);
|
|
833
|
+
} else {
|
|
834
|
+
return await runTransaction(
|
|
835
|
+
ClientAdapter.firestore,
|
|
836
|
+
performTransaction
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
} catch (err) {
|
|
840
|
+
if (err instanceof ClientAdapterError) {
|
|
841
|
+
throw err;
|
|
842
|
+
} else {
|
|
843
|
+
this._outputErrorConsole("restore", err);
|
|
844
|
+
throw new ClientAdapterError(ERRORS.SYSTEM_UNKNOWN_ERROR);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Unsubscribes from the active Firestore real-time listener, if one exists.
|
|
851
|
+
* - Also clears the local document array (`this.docs`).
|
|
852
|
+
*
|
|
853
|
+
* Firestore のリアルタイムリスナーを解除します。
|
|
854
|
+
* - 現在のリスナーが存在する場合、それを解除します。
|
|
855
|
+
* - さらに、`this.docs` に格納されていたドキュメントデータもクリアします。
|
|
856
|
+
*
|
|
857
|
+
* @returns {void}
|
|
858
|
+
*/
|
|
859
|
+
unsubscribe() {
|
|
860
|
+
if (this.listener) {
|
|
861
|
+
this.listener();
|
|
862
|
+
this.listener = null;
|
|
863
|
+
}
|
|
864
|
+
this.docs.splice(0);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Sets a real-time listener on a Firestore document and initializes the instance with its data.
|
|
869
|
+
* - If a listener already exists, it will be unsubscribed first.
|
|
870
|
+
*
|
|
871
|
+
* Firestore のドキュメントに対してリアルタイムリスナーを設定し、
|
|
872
|
+
* ドキュメントのデータでインスタンスを初期化します。
|
|
873
|
+
*
|
|
874
|
+
* @param {Object} args - Subscribe options.
|
|
875
|
+
* @param {string} args.docId - Document ID to subscribe to.
|
|
876
|
+
* @param {string|null} [args.prefix=null] - Optional path prefix.
|
|
877
|
+
* @param {function|null} [callback=null] - Callback executed on document changes (moved from args).
|
|
878
|
+
* @returns {void}
|
|
879
|
+
* @throws {Error} If docId is missing.
|
|
880
|
+
*/
|
|
881
|
+
subscribe({ docId, prefix = null } = {}, callback = null) {
|
|
882
|
+
this.unsubscribe();
|
|
883
|
+
|
|
884
|
+
if (!docId) {
|
|
885
|
+
throw new ClientAdapterError(ERRORS.VALIDATION_MISSING_DOC_ID);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
try {
|
|
889
|
+
const collectionPath = this.constructor.getCollectionPath(prefix);
|
|
890
|
+
// const colRef = collection(ClientAdapter.firestore, collectionPath);
|
|
891
|
+
const colRef = collection(
|
|
892
|
+
ClientAdapter.firestore,
|
|
893
|
+
collectionPath
|
|
894
|
+
).withConverter(this.constructor.converter());
|
|
895
|
+
const docRef = doc(colRef, docId);
|
|
896
|
+
this.listener = onSnapshot(docRef, (docSnapshot) => {
|
|
897
|
+
this.initialize(docSnapshot.data());
|
|
898
|
+
if (callback) callback(docSnapshot.data());
|
|
899
|
+
});
|
|
900
|
+
} catch (err) {
|
|
901
|
+
if (err instanceof ClientAdapterError) {
|
|
902
|
+
throw err;
|
|
903
|
+
} else {
|
|
904
|
+
this._outputErrorConsole("subscribe", err);
|
|
905
|
+
throw new ClientAdapterError(ERRORS.SYSTEM_UNKNOWN_ERROR);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Sets a real-time listener on a Firestore collection and monitors changes.
|
|
912
|
+
* - If `constraints` is a string, performs N-gram search using `tokenMap`.
|
|
913
|
+
* - If `constraints` is an array, applies Firestore query conditions.
|
|
914
|
+
* - If `prefix` is provided, it is used to resolve the collection path.
|
|
915
|
+
*
|
|
916
|
+
* @param {Object} args - Subscribe options.
|
|
917
|
+
* @param {Array|string} args.constraints - Query condition array or search string.
|
|
918
|
+
* @param {Array} [args.options=[]] - Additional query conditions.
|
|
919
|
+
* @param {string|null} [args.prefix=null] - Optional path prefix.
|
|
920
|
+
* @param {function|null} [args.callback=null] - [deprecated] Callback executed on document changes.
|
|
921
|
+
* @param {function|null} [callback=null] - Callback executed on document changes (moved from args).
|
|
922
|
+
* @returns {Array<Object>} Live-updated document data.
|
|
923
|
+
*/
|
|
924
|
+
subscribeDocs(
|
|
925
|
+
{
|
|
926
|
+
constraints = [],
|
|
927
|
+
options = [],
|
|
928
|
+
prefix = null,
|
|
929
|
+
callback: deprecatedCallback = null,
|
|
930
|
+
} = {},
|
|
931
|
+
callback = null
|
|
932
|
+
) {
|
|
933
|
+
/**
|
|
934
|
+
* [DEPRECATION NOTICE]
|
|
935
|
+
* - The `callback` parameter has been moved from the options object to a separate parameter.
|
|
936
|
+
* - Please update your code accordingly.
|
|
937
|
+
*/
|
|
938
|
+
if (deprecatedCallback) {
|
|
939
|
+
console.warn(
|
|
940
|
+
"[FireModel-subscribeDocs] The 'callback' parameter has been moved from the options object to a separate parameter. Please update your code accordingly."
|
|
941
|
+
);
|
|
942
|
+
if (!callback) {
|
|
943
|
+
callback = deprecatedCallback;
|
|
944
|
+
} else {
|
|
945
|
+
console.warn(
|
|
946
|
+
"[FireModel-subscribeDocs] The 'callback' parameter was provided both in the options object and as a separate parameter. The separate parameter will take precedence."
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
this.unsubscribe();
|
|
952
|
+
const queryConstraints = [];
|
|
953
|
+
|
|
954
|
+
if (typeof constraints === "string") {
|
|
955
|
+
queryConstraints.push(...this.createTokenMapQueries(constraints));
|
|
956
|
+
queryConstraints.push(...this.createQueries(options));
|
|
957
|
+
} else if (Array.isArray(constraints)) {
|
|
958
|
+
queryConstraints.push(...this.createQueries(constraints));
|
|
959
|
+
} else {
|
|
960
|
+
throw new ClientAdapterError(ERRORS.VALIDATION_INVALID_CONSTRAINTS);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
try {
|
|
964
|
+
const collectionPath = this.constructor.getCollectionPath(prefix);
|
|
965
|
+
const colRef = collection(
|
|
966
|
+
ClientAdapter.firestore,
|
|
967
|
+
collectionPath
|
|
968
|
+
).withConverter(this.constructor.converter());
|
|
969
|
+
const queryRef = query(colRef, ...queryConstraints);
|
|
970
|
+
|
|
971
|
+
this.listener = onSnapshot(queryRef, (snapshot) => {
|
|
972
|
+
snapshot.docChanges().forEach((change) => {
|
|
973
|
+
const item = change.doc.data();
|
|
974
|
+
const index = this.docs.findIndex(
|
|
975
|
+
({ docId }) => docId === item.docId
|
|
976
|
+
);
|
|
977
|
+
if (change.type === "added") this.docs.push(item);
|
|
978
|
+
if (change.type === "modified") this.docs.splice(index, 1, item);
|
|
979
|
+
if (change.type === "removed") this.docs.splice(index, 1);
|
|
980
|
+
if (callback) callback(item, change.type);
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
return this.docs;
|
|
985
|
+
} catch (err) {
|
|
986
|
+
if (err instanceof ClientAdapterError) {
|
|
987
|
+
throw err;
|
|
988
|
+
} else {
|
|
989
|
+
this._outputErrorConsole("subscribeDocs", err);
|
|
990
|
+
throw new ClientAdapterError(ERRORS.SYSTEM_UNKNOWN_ERROR);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
export default ClientAdapter;
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shisyamo4131/air-firebase-v2-client-adapter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "client adapter for FireModel",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"firebase",
|
|
15
|
+
"firestore",
|
|
16
|
+
"client",
|
|
17
|
+
"adapter"
|
|
18
|
+
],
|
|
19
|
+
"author": "shisyamo4131",
|
|
20
|
+
"license": "ISC",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/shisyamo4131/air-firebase-v2-client-adapter.git"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"firebase": "^10.0.0 || ^11.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|