qumra-pos-storage 0.1.0 → 0.1.1

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/dist/index.cjs CHANGED
@@ -124,10 +124,28 @@ ALTER TABLE pending_operations ADD COLUMN inflight_at TEXT;
124
124
  `
125
125
  );
126
126
 
127
+ // src/schema/003_documents.ts
128
+ var SQL_003 = (
129
+ /* sql */
130
+ `
131
+ CREATE TABLE documents (
132
+ id TEXT PRIMARY KEY, -- UUIDv7 \u0628\u064A\u062A\u0648\u0644\u0651\u062F \u062A\u0644\u0642\u0627\u0626\u064A\u0627\u064B (\u0645\u0631\u0633\u0627\u0629 \u0627\u0644\u0640 idempotency)
133
+ collection TEXT NOT NULL, -- "orders" | "products" | \u0623\u064A \u0627\u0633\u0645 \u0628\u062A\u0627\u0639\u0643
134
+ data TEXT NOT NULL, -- JSON \u2014 \u0623\u064A keys \u0625\u0646\u062A \u0639\u0627\u064A\u0632\u0647\u0627
135
+ created_at TEXT NOT NULL,
136
+ updated_at TEXT NOT NULL,
137
+ deleted INTEGER NOT NULL DEFAULT 0, -- \u062D\u0630\u0641 \u0646\u0627\u0639\u0645 (tombstone) \u2014 \u0627\u0644\u0628\u064A\u0627\u0646\u0627\u062A \u0645\u0627\u0628\u062A\u062A\u0645\u0633\u0651\u062D
138
+ sync_status TEXT NOT NULL DEFAULT 'pending'
139
+ );
140
+ CREATE INDEX idx_documents_collection ON documents(collection, updated_at);
141
+ `
142
+ );
143
+
127
144
  // src/schema/index.ts
128
145
  var MIGRATIONS = [
129
146
  { version: 1, sql: SQL_001 },
130
- { version: 2, sql: SQL_002 }
147
+ { version: 2, sql: SQL_002 },
148
+ { version: 3, sql: SQL_003 }
131
149
  ];
132
150
 
133
151
  // src/time.ts
@@ -160,6 +178,99 @@ async function runMigrations(adapter) {
160
178
  }
161
179
  var newId = () => uuidv7.uuidv7();
162
180
 
181
+ // src/repositories/DocumentsRepository.ts
182
+ var DocumentsRepository = class {
183
+ constructor(adapter, outbox) {
184
+ this.adapter = adapter;
185
+ this.outbox = outbox;
186
+ }
187
+ adapter;
188
+ outbox;
189
+ /** إنشاء مستند جديد بـ UUID تلقائي. بيرجّع المستند بالـ id. */
190
+ async create(collection, data) {
191
+ return this.write(collection, newId(), data, false);
192
+ }
193
+ /** إدراج/تحديث مستند بـ id معروف (لو موجود بيتحدّث، لو لأ بيتعمل). */
194
+ async upsert(collection, id, data) {
195
+ return this.write(collection, id, data, false);
196
+ }
197
+ /** حذف ناعم (tombstone) — البيانات مابتتمسّح، بس بتتعلّم deleted وتتزامن كحذف. */
198
+ async remove(collection, id) {
199
+ const existing = await this.get(collection, id);
200
+ if (!existing) return;
201
+ await this.write(collection, id, existing.data, true);
202
+ }
203
+ async get(collection, id) {
204
+ const rows = await this.adapter.query(
205
+ `SELECT * FROM documents WHERE id = ? AND collection = ?`,
206
+ [id, collection]
207
+ );
208
+ const row = rows[0];
209
+ return row ? this.toDoc(row) : null;
210
+ }
211
+ /** كل مستندات مجموعة (أحدث أولاً). بيتجاهل المحذوف افتراضياً. */
212
+ async all(collection, opts = {}) {
213
+ const limit = opts.limit ?? 100;
214
+ const offset = opts.offset ?? 0;
215
+ const guard = opts.includeDeleted ? "" : "AND deleted = 0";
216
+ const rows = await this.adapter.query(
217
+ `SELECT * FROM documents
218
+ WHERE collection = ? ${guard}
219
+ ORDER BY updated_at DESC
220
+ LIMIT ? OFFSET ?`,
221
+ [collection, limit, offset]
222
+ );
223
+ return rows.map((r) => this.toDoc(r));
224
+ }
225
+ async write(collection, id, data, deleted) {
226
+ const now = nowISO();
227
+ const dataJson = JSON.stringify(data);
228
+ const opType = deleted ? "doc.delete" : "doc.upsert";
229
+ await this.adapter.transaction(async (tx) => {
230
+ await tx.execute(
231
+ `INSERT INTO documents
232
+ (id, collection, data, created_at, updated_at, deleted, sync_status)
233
+ VALUES (?, ?, ?, ?, ?, ?, 'pending')
234
+ ON CONFLICT(id) DO UPDATE SET
235
+ data = excluded.data,
236
+ updated_at = excluded.updated_at,
237
+ deleted = excluded.deleted,
238
+ sync_status = 'pending'`,
239
+ [id, collection, dataJson, now, now, deleted ? 1 : 0]
240
+ );
241
+ await this.outbox.enqueue(tx, {
242
+ opType,
243
+ payload: { op: deleted ? "delete" : "upsert", id, collection, data, updatedAt: now },
244
+ // فريد لكل كتابة (opId جديد) علشان التحديثات المتتالية تتزامن كلها؛
245
+ // والـ docId جوه المفتاح علشان الـ settle يعكس sync_status على المستند.
246
+ idempotencyKey: `${opType}:${id}:${newId()}`,
247
+ clientTs: now
248
+ });
249
+ });
250
+ const stored = await this.get(collection, id);
251
+ return stored ?? {
252
+ id,
253
+ collection,
254
+ data,
255
+ createdAt: now,
256
+ updatedAt: now,
257
+ deleted,
258
+ syncStatus: "pending"
259
+ };
260
+ }
261
+ toDoc(row) {
262
+ return {
263
+ id: row.id,
264
+ collection: row.collection,
265
+ data: JSON.parse(row.data),
266
+ createdAt: row.created_at,
267
+ updatedAt: row.updated_at,
268
+ deleted: row.deleted === 1,
269
+ syncStatus: row.sync_status
270
+ };
271
+ }
272
+ };
273
+
163
274
  // src/repositories/OrdersRepository.ts
164
275
  var OrdersRepository = class {
165
276
  constructor(adapter, outbox) {
@@ -414,6 +525,14 @@ var OutboxRepository = class {
414
525
  outcome,
415
526
  orderId
416
527
  ]);
528
+ } else if (op.op_type === "doc.upsert" || op.op_type === "doc.delete") {
529
+ const docId = op.idempotency_key.slice(op.op_type.length + 1).split(":")[0];
530
+ if (docId) {
531
+ await tx.execute(`UPDATE documents SET sync_status = ? WHERE id = ?`, [
532
+ outcome,
533
+ docId
534
+ ]);
535
+ }
417
536
  }
418
537
  }
419
538
  });
@@ -731,6 +850,7 @@ async function createStorage(adapter) {
731
850
  orders: new OrdersRepository(adapter, outbox),
732
851
  products: new ProductsRepository(adapter, outbox),
733
852
  reports: new ReportsRepository(adapter),
853
+ docs: new DocumentsRepository(adapter, outbox),
734
854
  outbox,
735
855
  adapter,
736
856
  close: () => adapter.close()
@@ -1314,6 +1434,7 @@ exports.CASHIERS_RESOURCE = CASHIERS_RESOURCE;
1314
1434
  exports.CONFORMANCE_CASES = CONFORMANCE_CASES;
1315
1435
  exports.DEFAULT_BACKOFF = DEFAULT_BACKOFF;
1316
1436
  exports.DEFAULT_RESOURCES = DEFAULT_RESOURCES;
1437
+ exports.DocumentsRepository = DocumentsRepository;
1317
1438
  exports.MIGRATIONS = MIGRATIONS;
1318
1439
  exports.OrdersRepository = OrdersRepository;
1319
1440
  exports.OutboxRepository = OutboxRepository;