@neetru/sdk 2.3.3 → 2.3.5

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.mjs CHANGED
@@ -3,12 +3,6 @@ import { openDB } from 'idb';
3
3
 
4
4
  var __defProp = Object.defineProperty;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
7
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
8
- }) : x)(function(x) {
9
- if (typeof require !== "undefined") return require.apply(this, arguments);
10
- throw Error('Dynamic require of "' + x + '" is not supported');
11
- });
12
6
  var __esm = (fn, res) => function __init() {
13
7
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
14
8
  };
@@ -172,6 +166,405 @@ var init_http = __esm({
172
166
  }
173
167
  });
174
168
 
169
+ // src/db-errors.ts
170
+ var RETRYABLE_CODES2, NeetruDbError;
171
+ var init_db_errors = __esm({
172
+ "src/db-errors.ts"() {
173
+ init_errors();
174
+ RETRYABLE_CODES2 = /* @__PURE__ */ new Set([
175
+ "db_unavailable",
176
+ "db_conflict",
177
+ "db_timeout"
178
+ ]);
179
+ NeetruDbError = class _NeetruDbError extends NeetruError {
180
+ /** Código de erro fechado — específico de DB. */
181
+ code;
182
+ /**
183
+ * `true` para erros transientes que o produto pode tentar novamente.
184
+ * São retryable: `db_unavailable`, `db_conflict`, `db_timeout`.
185
+ */
186
+ retryable;
187
+ /**
188
+ * ID opaco do banco lógico — só para correlação com logs do Core.
189
+ * Nunca deve ser exibido ao usuário final.
190
+ */
191
+ dbId;
192
+ constructor(code, message, dbId) {
193
+ super(code, message);
194
+ this.name = "NeetruDbError";
195
+ this.code = code;
196
+ this.retryable = RETRYABLE_CODES2.has(code);
197
+ this.dbId = dbId;
198
+ Object.setPrototypeOf(this, _NeetruDbError.prototype);
199
+ }
200
+ };
201
+ }
202
+ });
203
+
204
+ // src/db/sql/lease.ts
205
+ function mapPoolError(err) {
206
+ if (err instanceof NeetruDbError) return err;
207
+ const message = err instanceof Error ? err.message : String(err);
208
+ const code = err instanceof Error ? err.code : void 0;
209
+ if (typeof code === "string" && (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT")) {
210
+ return new NeetruDbError(
211
+ "db_unavailable",
212
+ `Pool connection failed (${code}): ${message}`
213
+ );
214
+ }
215
+ if (message.includes("statement timeout") || message.includes("lock timeout")) {
216
+ return new NeetruDbError("db_timeout", `Query timeout: ${message}`);
217
+ }
218
+ if (message.includes("40001") || message.includes("40P01") || message.includes("deadlock")) {
219
+ return new NeetruDbError("db_conflict", `Transaction conflict: ${message}`);
220
+ }
221
+ if (message.includes("42501") || message.toLowerCase().includes("permission denied")) {
222
+ return new NeetruDbError("db_permission_denied", `Permission denied: ${message}`);
223
+ }
224
+ return new NeetruDbError("db_unavailable", `Database error: ${message}`);
225
+ }
226
+ var RENEWAL_THRESHOLD, MIN_RENEWAL_DELAY_MS, SqlLeaseManager;
227
+ var init_lease = __esm({
228
+ "src/db/sql/lease.ts"() {
229
+ init_db_errors();
230
+ RENEWAL_THRESHOLD = 0.8;
231
+ MIN_RENEWAL_DELAY_MS = 3e4;
232
+ SqlLeaseManager = class {
233
+ _lease;
234
+ _pool;
235
+ _orm;
236
+ _renewalTimer = null;
237
+ _closed = false;
238
+ _deps;
239
+ constructor(lease, pool, orm, deps) {
240
+ this._lease = lease;
241
+ this._pool = pool;
242
+ this._orm = orm;
243
+ this._deps = {
244
+ ...deps,
245
+ now: deps.now ?? (() => Date.now())
246
+ };
247
+ this._scheduleRenewal();
248
+ }
249
+ // ─── Superfície pública (NeetruSqlClient) ──────────────────────────────────
250
+ get orm() {
251
+ this._assertOpen();
252
+ return this._orm;
253
+ }
254
+ async transaction(fn, opts) {
255
+ this._assertOpen();
256
+ try {
257
+ if (opts?.isolationLevel) {
258
+ return await this._orm.transaction(fn, {
259
+ isolationLevel: opts.isolationLevel
260
+ });
261
+ }
262
+ return await this._orm.transaction(fn);
263
+ } catch (err) {
264
+ throw mapPoolError(err);
265
+ }
266
+ }
267
+ async close() {
268
+ if (this._closed) return;
269
+ this._closed = true;
270
+ this._cancelRenewal();
271
+ await this._pool.end().catch(() => {
272
+ });
273
+ }
274
+ // ─── Renovação proativa ────────────────────────────────────────────────────
275
+ /**
276
+ * Calcula o delay de renovação: momento de ~80% do TTL restante.
277
+ *
278
+ * Fórmula: `renewAt = issuedAt + TTL * RENEWAL_THRESHOLD`
279
+ * = `expiresAt - TTL * (1 - RENEWAL_THRESHOLD)`
280
+ *
281
+ * Se o lease já está além do ponto de renovação (ou TTL não calculável),
282
+ * usa `MIN_RENEWAL_DELAY_MS` como fallback seguro.
283
+ */
284
+ _calcRenewalDelayMs() {
285
+ const expiresAtMs = Date.parse(this._lease.expiresAt);
286
+ if (!Number.isFinite(expiresAtMs)) return MIN_RENEWAL_DELAY_MS;
287
+ const now = this._deps.now();
288
+ const remainingMs = expiresAtMs - now;
289
+ if (remainingMs <= 0) return MIN_RENEWAL_DELAY_MS;
290
+ const renewInMs = remainingMs * (1 - RENEWAL_THRESHOLD);
291
+ return Math.max(MIN_RENEWAL_DELAY_MS, Math.round(renewInMs));
292
+ }
293
+ _scheduleRenewal() {
294
+ if (this._closed) return;
295
+ const delayMs = this._calcRenewalDelayMs();
296
+ this._renewalTimer = setTimeout(() => {
297
+ void this._doRenew();
298
+ }, delayMs);
299
+ }
300
+ _cancelRenewal() {
301
+ if (this._renewalTimer !== null) {
302
+ clearTimeout(this._renewalTimer);
303
+ this._renewalTimer = null;
304
+ }
305
+ }
306
+ async _doRenew() {
307
+ if (this._closed) return;
308
+ try {
309
+ const renewOpts = {
310
+ leaseId: this._lease.leaseId
311
+ };
312
+ if (this._deps.database !== void 0) {
313
+ renewOpts.database = this._deps.database;
314
+ }
315
+ const newLease = await this._deps.fetchLease(renewOpts);
316
+ await this._swapPool(newLease);
317
+ } catch (err) {
318
+ this._renewalTimer = setTimeout(() => {
319
+ void this._doRenew();
320
+ }, MIN_RENEWAL_DELAY_MS);
321
+ if (process.env["NODE_ENV"] !== "production") {
322
+ console.warn("[SqlLeaseManager] Lease renewal failed, will retry:", err);
323
+ }
324
+ return;
325
+ }
326
+ this._scheduleRenewal();
327
+ }
328
+ /**
329
+ * Hot-swap do pool: cria pool novo, atualiza ponteiros, drena pool antigo.
330
+ *
331
+ * Exposto como método protegido para testes.
332
+ */
333
+ async _swapPool(newLease) {
334
+ const credRotated = newLease.credentialVersion !== this._lease.credentialVersion;
335
+ if (!credRotated && newLease.leaseId === this._lease.leaseId) {
336
+ this._lease = newLease;
337
+ return;
338
+ }
339
+ const newPool = this._deps.createPool(newLease);
340
+ const newOrm = this._deps.createDrizzle(newPool, this._deps.schema);
341
+ const oldPool = this._pool;
342
+ this._lease = newLease;
343
+ this._pool = newPool;
344
+ this._orm = newOrm;
345
+ void oldPool.end().catch(() => {
346
+ });
347
+ }
348
+ // ─── Helpers ───────────────────────────────────────────────────────────────
349
+ _assertOpen() {
350
+ if (this._closed) {
351
+ throw new NeetruDbError(
352
+ "db_unavailable",
353
+ "SqlLeaseManager foi fechado \u2014 chame close() apenas no shutdown."
354
+ );
355
+ }
356
+ }
357
+ };
358
+ }
359
+ });
360
+
361
+ // src/db/sql/sql-client.ts
362
+ var sql_client_exports = {};
363
+ __export(sql_client_exports, {
364
+ LEASE_ENDPOINT: () => LEASE_ENDPOINT,
365
+ LEASE_RENEW_ENDPOINT: () => LEASE_RENEW_ENDPOINT,
366
+ createDrizzleHandle: () => createDrizzleHandle,
367
+ createHttpLeaseFetcher: () => createHttpLeaseFetcher,
368
+ createPgPool: () => createPgPool,
369
+ createSqlClientFromConfig: () => createSqlClientFromConfig,
370
+ createSqlClientWithDeps: () => createSqlClientWithDeps
371
+ });
372
+ function mapEnvToCore(sdkEnv) {
373
+ if (sdkEnv === "workspace") return "staging";
374
+ if (sdkEnv === "prod") return "production";
375
+ return "dev";
376
+ }
377
+ function createHttpLeaseFetcher(config) {
378
+ return async (opts) => {
379
+ const { httpRequest: httpRequest2 } = await Promise.resolve().then(() => (init_http(), http_exports));
380
+ const isRenewal = Boolean(opts?.leaseId);
381
+ const path = isRenewal ? LEASE_RENEW_ENDPOINT : LEASE_ENDPOINT;
382
+ let body;
383
+ if (isRenewal) {
384
+ body = { leaseId: opts.leaseId };
385
+ } else {
386
+ body = {
387
+ productId: config.productId ?? "",
388
+ environment: mapEnvToCore(config.env ?? "prod")
389
+ };
390
+ if (opts?.database !== void 0) {
391
+ body["database"] = opts.database;
392
+ }
393
+ }
394
+ let raw;
395
+ try {
396
+ raw = await httpRequest2(config, {
397
+ method: "POST",
398
+ path,
399
+ body,
400
+ requireAuth: true,
401
+ // Lease fetch é idempotente do ponto de vista de segurança — retries OK.
402
+ retries: 2
403
+ });
404
+ } catch (err) {
405
+ const msg = err instanceof Error ? err.message : String(err);
406
+ throw new NeetruDbError(
407
+ "db_unavailable",
408
+ `Falha ao ${isRenewal ? "renovar" : "obter"} lease SQL: ${msg}`
409
+ );
410
+ }
411
+ return parseLease(raw);
412
+ };
413
+ }
414
+ function parseLease(raw) {
415
+ if (!raw || typeof raw !== "object") {
416
+ throw new NeetruDbError("db_unavailable", "Resposta de lease inv\xE1lida (n\xE3o \xE9 objeto).");
417
+ }
418
+ const envelope = raw;
419
+ const leaseObj = envelope["lease"];
420
+ if (!leaseObj || typeof leaseObj !== "object") {
421
+ throw new NeetruDbError(
422
+ "db_unavailable",
423
+ 'Resposta de lease inv\xE1lida: campo "lease" ausente ou n\xE3o \xE9 objeto. O Core retorna { kind, lease: { leaseId, host, ... } }.'
424
+ );
425
+ }
426
+ const r = leaseObj;
427
+ function req(field, type) {
428
+ if (typeof r[field] !== type) {
429
+ throw new NeetruDbError(
430
+ "db_unavailable",
431
+ `Campo obrigat\xF3rio "${field}" ausente ou tipo inv\xE1lido na resposta de lease.`
432
+ );
433
+ }
434
+ return r[field];
435
+ }
436
+ return {
437
+ leaseId: req("leaseId", "string"),
438
+ host: req("host", "string"),
439
+ port: typeof r["port"] === "number" ? r["port"] : 5433,
440
+ dbName: req("dbName", "string"),
441
+ user: req("user", "string"),
442
+ password: req("password", "string"),
443
+ sslca: typeof r["sslca"] === "string" ? r["sslca"] : null,
444
+ clientCert: typeof r["clientCert"] === "string" ? r["clientCert"] : null,
445
+ clientKey: typeof r["clientKey"] === "string" ? r["clientKey"] : null,
446
+ credentialVersion: typeof r["credentialVersion"] === "number" ? r["credentialVersion"] : 1,
447
+ expiresAt: req("expiresAt", "string")
448
+ };
449
+ }
450
+ async function createPgPool(lease) {
451
+ const { Pool } = await import('pg');
452
+ const ssl = lease.sslca ? {
453
+ ca: lease.sslca,
454
+ cert: lease.clientCert ?? void 0,
455
+ key: lease.clientKey ?? void 0,
456
+ rejectUnauthorized: true
457
+ } : false;
458
+ return new Pool({
459
+ host: lease.host,
460
+ port: lease.port,
461
+ database: lease.dbName,
462
+ user: lease.user,
463
+ password: lease.password,
464
+ ssl,
465
+ max: 3,
466
+ idleTimeoutMillis: 3e4,
467
+ connectionTimeoutMillis: 1e4
468
+ });
469
+ }
470
+ async function createDrizzleHandle(pool, schema) {
471
+ const { drizzle } = await import('drizzle-orm/node-postgres');
472
+ return drizzle(pool, { schema });
473
+ }
474
+ async function createSqlClientWithDeps(deps, options) {
475
+ const depsWithDb = options?.database !== void 0 ? { ...deps, database: options.database } : deps;
476
+ let lease;
477
+ try {
478
+ if (options?.database !== void 0) {
479
+ lease = await depsWithDb.fetchLease({ database: options.database });
480
+ } else {
481
+ lease = await depsWithDb.fetchLease();
482
+ }
483
+ } catch (err) {
484
+ if (err instanceof NeetruDbError) throw err;
485
+ const msg = err instanceof Error ? err.message : String(err);
486
+ throw new NeetruDbError("db_unavailable", `Falha ao obter lease inicial: ${msg}`);
487
+ }
488
+ let pool;
489
+ try {
490
+ pool = depsWithDb.createPool(lease);
491
+ } catch (err) {
492
+ const msg = err instanceof Error ? err.message : String(err);
493
+ throw new NeetruDbError("db_unavailable", `Falha ao abrir pool: ${msg}`);
494
+ }
495
+ let orm;
496
+ try {
497
+ orm = depsWithDb.createDrizzle(pool, depsWithDb.schema);
498
+ } catch (err) {
499
+ await pool.end().catch(() => {
500
+ });
501
+ const msg = err instanceof Error ? err.message : String(err);
502
+ throw new NeetruDbError("db_unavailable", `Falha ao criar handle Drizzle: ${msg}`);
503
+ }
504
+ return new SqlLeaseManager(lease, pool, orm, depsWithDb);
505
+ }
506
+ async function createSqlClientFromConfig(config, schema, options) {
507
+ const fetchLease = createHttpLeaseFetcher(config);
508
+ let pgMod;
509
+ let drizzlePgMod;
510
+ try {
511
+ pgMod = await import(
512
+ /* webpackIgnore: true */
513
+ 'pg'
514
+ );
515
+ drizzlePgMod = await import(
516
+ /* webpackIgnore: true */
517
+ 'drizzle-orm/node-postgres'
518
+ );
519
+ } catch (err) {
520
+ throw new NeetruDbError(
521
+ "db_unavailable",
522
+ `[client.db.sql] requer pg + drizzle-orm como peerDependencies do servidor. Instale: npm install pg drizzle-orm. (${err.message})`
523
+ );
524
+ }
525
+ const createPool = (lease) => {
526
+ const Pool = pgMod.Pool ?? pgMod.default?.Pool;
527
+ const ssl = lease.sslca ? {
528
+ ca: lease.sslca,
529
+ cert: lease.clientCert ?? void 0,
530
+ key: lease.clientKey ?? void 0,
531
+ rejectUnauthorized: true
532
+ } : false;
533
+ return new Pool({
534
+ host: lease.host,
535
+ port: lease.port,
536
+ database: lease.dbName,
537
+ user: lease.user,
538
+ password: lease.password,
539
+ ssl,
540
+ max: 3,
541
+ idleTimeoutMillis: 3e4,
542
+ connectionTimeoutMillis: 1e4
543
+ });
544
+ };
545
+ const createDrizzle = (pool, s) => {
546
+ return drizzlePgMod.drizzle(pool, { schema: s });
547
+ };
548
+ return createSqlClientWithDeps(
549
+ {
550
+ fetchLease,
551
+ createPool,
552
+ createDrizzle,
553
+ schema
554
+ },
555
+ options
556
+ );
557
+ }
558
+ var LEASE_ENDPOINT, LEASE_RENEW_ENDPOINT;
559
+ var init_sql_client = __esm({
560
+ "src/db/sql/sql-client.ts"() {
561
+ init_db_errors();
562
+ init_lease();
563
+ LEASE_ENDPOINT = "/api/sdk/v1/db/lease";
564
+ LEASE_RENEW_ENDPOINT = "/api/sdk/v1/db/lease/renew";
565
+ }
566
+ });
567
+
175
568
  // src/auth.ts
176
569
  init_errors();
177
570
 
@@ -751,35 +1144,8 @@ function createSupportNamespace(config) {
751
1144
  };
752
1145
  }
753
1146
 
754
- // src/db-errors.ts
755
- init_errors();
756
- var RETRYABLE_CODES2 = /* @__PURE__ */ new Set([
757
- "db_unavailable",
758
- "db_conflict",
759
- "db_timeout"
760
- ]);
761
- var NeetruDbError = class _NeetruDbError extends NeetruError {
762
- /** Código de erro fechado — específico de DB. */
763
- code;
764
- /**
765
- * `true` para erros transientes que o produto pode tentar novamente.
766
- * São retryable: `db_unavailable`, `db_conflict`, `db_timeout`.
767
- */
768
- retryable;
769
- /**
770
- * ID opaco do banco lógico — só para correlação com logs do Core.
771
- * Nunca deve ser exibido ao usuário final.
772
- */
773
- dbId;
774
- constructor(code, message, dbId) {
775
- super(code, message);
776
- this.name = "NeetruDbError";
777
- this.code = code;
778
- this.retryable = RETRYABLE_CODES2.has(code);
779
- this.dbId = dbId;
780
- Object.setPrototypeOf(this, _NeetruDbError.prototype);
781
- }
782
- };
1147
+ // src/db/client-db.ts
1148
+ init_db_errors();
783
1149
 
784
1150
  // src/db/offline/query-engine.ts
785
1151
  function typeRank(v) {
@@ -2826,6 +3192,7 @@ var TabCoordinator = class {
2826
3192
  };
2827
3193
 
2828
3194
  // src/db/collection-ref.ts
3195
+ init_db_errors();
2829
3196
  function isIndexedDBUnavailable() {
2830
3197
  return typeof indexedDB === "undefined" || typeof window === "undefined";
2831
3198
  }
@@ -3411,307 +3778,6 @@ async function createOfflineDocumentsNamespace(opts) {
3411
3778
  });
3412
3779
  }
3413
3780
 
3414
- // src/db/sql/lease.ts
3415
- var RENEWAL_THRESHOLD = 0.8;
3416
- var MIN_RENEWAL_DELAY_MS = 3e4;
3417
- var SqlLeaseManager = class {
3418
- _lease;
3419
- _pool;
3420
- _orm;
3421
- _renewalTimer = null;
3422
- _closed = false;
3423
- _deps;
3424
- constructor(lease, pool, orm, deps) {
3425
- this._lease = lease;
3426
- this._pool = pool;
3427
- this._orm = orm;
3428
- this._deps = {
3429
- ...deps,
3430
- now: deps.now ?? (() => Date.now())
3431
- };
3432
- this._scheduleRenewal();
3433
- }
3434
- // ─── Superfície pública (NeetruSqlClient) ──────────────────────────────────
3435
- get orm() {
3436
- this._assertOpen();
3437
- return this._orm;
3438
- }
3439
- async transaction(fn, opts) {
3440
- this._assertOpen();
3441
- try {
3442
- if (opts?.isolationLevel) {
3443
- return await this._orm.transaction(fn, {
3444
- isolationLevel: opts.isolationLevel
3445
- });
3446
- }
3447
- return await this._orm.transaction(fn);
3448
- } catch (err) {
3449
- throw mapPoolError(err);
3450
- }
3451
- }
3452
- async close() {
3453
- if (this._closed) return;
3454
- this._closed = true;
3455
- this._cancelRenewal();
3456
- await this._pool.end().catch(() => {
3457
- });
3458
- }
3459
- // ─── Renovação proativa ────────────────────────────────────────────────────
3460
- /**
3461
- * Calcula o delay de renovação: momento de ~80% do TTL restante.
3462
- *
3463
- * Fórmula: `renewAt = issuedAt + TTL * RENEWAL_THRESHOLD`
3464
- * = `expiresAt - TTL * (1 - RENEWAL_THRESHOLD)`
3465
- *
3466
- * Se o lease já está além do ponto de renovação (ou TTL não calculável),
3467
- * usa `MIN_RENEWAL_DELAY_MS` como fallback seguro.
3468
- */
3469
- _calcRenewalDelayMs() {
3470
- const expiresAtMs = Date.parse(this._lease.expiresAt);
3471
- if (!Number.isFinite(expiresAtMs)) return MIN_RENEWAL_DELAY_MS;
3472
- const now = this._deps.now();
3473
- const remainingMs = expiresAtMs - now;
3474
- if (remainingMs <= 0) return MIN_RENEWAL_DELAY_MS;
3475
- const renewInMs = remainingMs * (1 - RENEWAL_THRESHOLD);
3476
- return Math.max(MIN_RENEWAL_DELAY_MS, Math.round(renewInMs));
3477
- }
3478
- _scheduleRenewal() {
3479
- if (this._closed) return;
3480
- const delayMs = this._calcRenewalDelayMs();
3481
- this._renewalTimer = setTimeout(() => {
3482
- void this._doRenew();
3483
- }, delayMs);
3484
- }
3485
- _cancelRenewal() {
3486
- if (this._renewalTimer !== null) {
3487
- clearTimeout(this._renewalTimer);
3488
- this._renewalTimer = null;
3489
- }
3490
- }
3491
- async _doRenew() {
3492
- if (this._closed) return;
3493
- try {
3494
- const renewOpts = {
3495
- leaseId: this._lease.leaseId
3496
- };
3497
- if (this._deps.database !== void 0) {
3498
- renewOpts.database = this._deps.database;
3499
- }
3500
- const newLease = await this._deps.fetchLease(renewOpts);
3501
- await this._swapPool(newLease);
3502
- } catch (err) {
3503
- this._renewalTimer = setTimeout(() => {
3504
- void this._doRenew();
3505
- }, MIN_RENEWAL_DELAY_MS);
3506
- if (process.env["NODE_ENV"] !== "production") {
3507
- console.warn("[SqlLeaseManager] Lease renewal failed, will retry:", err);
3508
- }
3509
- return;
3510
- }
3511
- this._scheduleRenewal();
3512
- }
3513
- /**
3514
- * Hot-swap do pool: cria pool novo, atualiza ponteiros, drena pool antigo.
3515
- *
3516
- * Exposto como método protegido para testes.
3517
- */
3518
- async _swapPool(newLease) {
3519
- const credRotated = newLease.credentialVersion !== this._lease.credentialVersion;
3520
- if (!credRotated && newLease.leaseId === this._lease.leaseId) {
3521
- this._lease = newLease;
3522
- return;
3523
- }
3524
- const newPool = this._deps.createPool(newLease);
3525
- const newOrm = this._deps.createDrizzle(newPool, this._deps.schema);
3526
- const oldPool = this._pool;
3527
- this._lease = newLease;
3528
- this._pool = newPool;
3529
- this._orm = newOrm;
3530
- void oldPool.end().catch(() => {
3531
- });
3532
- }
3533
- // ─── Helpers ───────────────────────────────────────────────────────────────
3534
- _assertOpen() {
3535
- if (this._closed) {
3536
- throw new NeetruDbError(
3537
- "db_unavailable",
3538
- "SqlLeaseManager foi fechado \u2014 chame close() apenas no shutdown."
3539
- );
3540
- }
3541
- }
3542
- };
3543
- function mapPoolError(err) {
3544
- if (err instanceof NeetruDbError) return err;
3545
- const message = err instanceof Error ? err.message : String(err);
3546
- const code = err instanceof Error ? err.code : void 0;
3547
- if (typeof code === "string" && (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT")) {
3548
- return new NeetruDbError(
3549
- "db_unavailable",
3550
- `Pool connection failed (${code}): ${message}`
3551
- );
3552
- }
3553
- if (message.includes("statement timeout") || message.includes("lock timeout")) {
3554
- return new NeetruDbError("db_timeout", `Query timeout: ${message}`);
3555
- }
3556
- if (message.includes("40001") || message.includes("40P01") || message.includes("deadlock")) {
3557
- return new NeetruDbError("db_conflict", `Transaction conflict: ${message}`);
3558
- }
3559
- if (message.includes("42501") || message.toLowerCase().includes("permission denied")) {
3560
- return new NeetruDbError("db_permission_denied", `Permission denied: ${message}`);
3561
- }
3562
- return new NeetruDbError("db_unavailable", `Database error: ${message}`);
3563
- }
3564
-
3565
- // src/db/sql/sql-client.ts
3566
- var LEASE_ENDPOINT = "/api/sdk/v1/db/lease";
3567
- var LEASE_RENEW_ENDPOINT = "/api/sdk/v1/db/lease/renew";
3568
- function mapEnvToCore(sdkEnv) {
3569
- if (sdkEnv === "workspace") return "staging";
3570
- if (sdkEnv === "prod") return "production";
3571
- return "dev";
3572
- }
3573
- function createHttpLeaseFetcher(config) {
3574
- return async (opts) => {
3575
- const { httpRequest: httpRequest2 } = await Promise.resolve().then(() => (init_http(), http_exports));
3576
- const isRenewal = Boolean(opts?.leaseId);
3577
- const path = isRenewal ? LEASE_RENEW_ENDPOINT : LEASE_ENDPOINT;
3578
- let body;
3579
- if (isRenewal) {
3580
- body = { leaseId: opts.leaseId };
3581
- } else {
3582
- body = {
3583
- productId: config.productId ?? "",
3584
- environment: mapEnvToCore(config.env ?? "prod")
3585
- };
3586
- if (opts?.database !== void 0) {
3587
- body["database"] = opts.database;
3588
- }
3589
- }
3590
- let raw;
3591
- try {
3592
- raw = await httpRequest2(config, {
3593
- method: "POST",
3594
- path,
3595
- body,
3596
- requireAuth: true,
3597
- // Lease fetch é idempotente do ponto de vista de segurança — retries OK.
3598
- retries: 2
3599
- });
3600
- } catch (err) {
3601
- const msg = err instanceof Error ? err.message : String(err);
3602
- throw new NeetruDbError(
3603
- "db_unavailable",
3604
- `Falha ao ${isRenewal ? "renovar" : "obter"} lease SQL: ${msg}`
3605
- );
3606
- }
3607
- return parseLease(raw);
3608
- };
3609
- }
3610
- function parseLease(raw) {
3611
- if (!raw || typeof raw !== "object") {
3612
- throw new NeetruDbError("db_unavailable", "Resposta de lease inv\xE1lida (n\xE3o \xE9 objeto).");
3613
- }
3614
- const envelope = raw;
3615
- const leaseObj = envelope["lease"];
3616
- if (!leaseObj || typeof leaseObj !== "object") {
3617
- throw new NeetruDbError(
3618
- "db_unavailable",
3619
- 'Resposta de lease inv\xE1lida: campo "lease" ausente ou n\xE3o \xE9 objeto. O Core retorna { kind, lease: { leaseId, host, ... } }.'
3620
- );
3621
- }
3622
- const r = leaseObj;
3623
- function req(field, type) {
3624
- if (typeof r[field] !== type) {
3625
- throw new NeetruDbError(
3626
- "db_unavailable",
3627
- `Campo obrigat\xF3rio "${field}" ausente ou tipo inv\xE1lido na resposta de lease.`
3628
- );
3629
- }
3630
- return r[field];
3631
- }
3632
- return {
3633
- leaseId: req("leaseId", "string"),
3634
- host: req("host", "string"),
3635
- port: typeof r["port"] === "number" ? r["port"] : 5433,
3636
- dbName: req("dbName", "string"),
3637
- user: req("user", "string"),
3638
- password: req("password", "string"),
3639
- sslca: typeof r["sslca"] === "string" ? r["sslca"] : null,
3640
- clientCert: typeof r["clientCert"] === "string" ? r["clientCert"] : null,
3641
- clientKey: typeof r["clientKey"] === "string" ? r["clientKey"] : null,
3642
- credentialVersion: typeof r["credentialVersion"] === "number" ? r["credentialVersion"] : 1,
3643
- expiresAt: req("expiresAt", "string")
3644
- };
3645
- }
3646
- async function createSqlClientWithDeps(deps, options) {
3647
- const depsWithDb = options?.database !== void 0 ? { ...deps, database: options.database } : deps;
3648
- let lease;
3649
- try {
3650
- if (options?.database !== void 0) {
3651
- lease = await depsWithDb.fetchLease({ database: options.database });
3652
- } else {
3653
- lease = await depsWithDb.fetchLease();
3654
- }
3655
- } catch (err) {
3656
- if (err instanceof NeetruDbError) throw err;
3657
- const msg = err instanceof Error ? err.message : String(err);
3658
- throw new NeetruDbError("db_unavailable", `Falha ao obter lease inicial: ${msg}`);
3659
- }
3660
- let pool;
3661
- try {
3662
- pool = depsWithDb.createPool(lease);
3663
- } catch (err) {
3664
- const msg = err instanceof Error ? err.message : String(err);
3665
- throw new NeetruDbError("db_unavailable", `Falha ao abrir pool: ${msg}`);
3666
- }
3667
- let orm;
3668
- try {
3669
- orm = depsWithDb.createDrizzle(pool, depsWithDb.schema);
3670
- } catch (err) {
3671
- await pool.end().catch(() => {
3672
- });
3673
- const msg = err instanceof Error ? err.message : String(err);
3674
- throw new NeetruDbError("db_unavailable", `Falha ao criar handle Drizzle: ${msg}`);
3675
- }
3676
- return new SqlLeaseManager(lease, pool, orm, depsWithDb);
3677
- }
3678
- async function createSqlClientFromConfig(config, schema, options) {
3679
- const fetchLease = createHttpLeaseFetcher(config);
3680
- const createPool = (lease) => {
3681
- const { Pool } = __require("pg");
3682
- const ssl = lease.sslca ? {
3683
- ca: lease.sslca,
3684
- cert: lease.clientCert ?? void 0,
3685
- key: lease.clientKey ?? void 0,
3686
- rejectUnauthorized: true
3687
- } : false;
3688
- return new Pool({
3689
- host: lease.host,
3690
- port: lease.port,
3691
- database: lease.dbName,
3692
- user: lease.user,
3693
- password: lease.password,
3694
- ssl,
3695
- max: 3,
3696
- idleTimeoutMillis: 3e4,
3697
- connectionTimeoutMillis: 1e4
3698
- });
3699
- };
3700
- const createDrizzle = (pool, s) => {
3701
- const { drizzle } = __require("drizzle-orm/node-postgres");
3702
- return drizzle(pool, { schema: s });
3703
- };
3704
- return createSqlClientWithDeps(
3705
- {
3706
- fetchLease,
3707
- createPool,
3708
- createDrizzle,
3709
- schema
3710
- },
3711
- options
3712
- );
3713
- }
3714
-
3715
3781
  // src/db/realtime/realtime-client.ts
3716
3782
  var WS_OPEN = 1;
3717
3783
  var WS_CLOSED = 3;
@@ -4334,7 +4400,8 @@ function createNeetruDb(config, dbOpts = {}) {
4334
4400
  "[SDK] db.sql() n\xE3o dispon\xEDvel em NEETRU_ENV=dev. Use `neetru dev` para subir o container Postgres local."
4335
4401
  );
4336
4402
  }
4337
- return createSqlClientFromConfig(config, schema, options);
4403
+ const sqlClientMod = await Promise.resolve().then(() => (init_sql_client(), sql_client_exports));
4404
+ return sqlClientMod.createSqlClientFromConfig(config, schema, options);
4338
4405
  },
4339
4406
  get syncState() {
4340
4407
  return _docsResolved?.syncState ?? {
@@ -4939,6 +5006,7 @@ var MockNotifications = class {
4939
5006
  };
4940
5007
 
4941
5008
  // src/mocks.ts
5009
+ init_db_errors();
4942
5010
  var DEV_FIXTURE_USER = Object.freeze({
4943
5011
  uid: "dev-fixture-uid-0001",
4944
5012
  email: "dev@neetru.local",
@@ -5368,21 +5436,46 @@ var MockDb = class {
5368
5436
  return ref;
5369
5437
  }
5370
5438
  /**
5371
- * Implementa SQL in-memory via `pg-mem` + `drizzle-orm/node-postgres`.
5439
+ * Implementa SQL in-memory para testes preferência: `@electric-sql/pglite`
5440
+ * + `drizzle-orm/pglite`; fallback: `pg-mem` + `drizzle-orm/node-postgres`
5441
+ * com strip interceptor.
5372
5442
  *
5373
- * bug_a7f2ae25fda54129b52b6dd55aaa6050 (HIGH 26/05): antes lançava
5374
- * `db_unavailable` sempre, bloqueando vitest server-side dos consumers
5375
- * que refatoraram pra `client.db.sql()`. Agora carrega `pg-mem` lazy +
5376
- * cria `NodePgDatabase` Drizzle apontando pra DB in-memory.
5443
+ * bug_6bae8674461f4600809e2f9de13c6043 (HIGH 26/05): fix anterior (2.3.3)
5444
+ * teve efeito INVERSO. Injetar `pool.types.getTypeParser` fez drizzle-orm
5445
+ * passar `query.types.getTypeParser` pro pg-mem, que tem guard interno
5446
+ * `if (query.types?.getTypeParser) throw NotSupported`. Ou seja: presença
5447
+ * de getTypeParser é o gatilho do erro — não ausência.
5377
5448
  *
5378
- * Requer `pg-mem`, `drizzle-orm` e `pg` como devDependencies do consumer
5379
- * (instale com `npm install -D pg-mem drizzle-orm pg`). Se ausentes, joga
5380
- * `db_unavailable` com mensagem clara.
5449
+ * Solução A (pglite, preferida): `@electric-sql/pglite` é WASM-Postgres
5450
+ * nativo sem adapter pg-mem, sem esse conflito de tipos.
5451
+ * Solução B (fallback pg-mem): strip interceptor no pool.query que deleta
5452
+ * `types.getTypeParser` do objeto de query ANTES do pg-mem ver. Compat com
5453
+ * consumers que só têm pg-mem nas devDeps.
5381
5454
  *
5382
- * Use `options.initSql` pra criar tabelas no test setup sem isso o orm
5383
- * funciona mas qualquer SELECT/INSERT em tabela inexistente vai falhar.
5455
+ * Use `options.initSql` pra rodar DDL no setup (CREATE TABLE etc).
5384
5456
  */
5385
5457
  async sql(schema, options) {
5458
+ const ddls = options?.initSql ? Array.isArray(options.initSql) ? options.initSql : [options.initSql] : [];
5459
+ try {
5460
+ const pglitePkg = await import('@electric-sql/pglite');
5461
+ const drizzlePglitePkg = await import('drizzle-orm/pglite');
5462
+ const pglite = await pglitePkg.PGlite.create();
5463
+ for (const ddl of ddls) {
5464
+ await pglite.query(ddl);
5465
+ }
5466
+ const pgliteOrm = drizzlePglitePkg.drizzle(pglite, { schema });
5467
+ const ormAsNode = pgliteOrm;
5468
+ return {
5469
+ orm: ormAsNode,
5470
+ async transaction(fn, _opts) {
5471
+ return await pgliteOrm.transaction(fn);
5472
+ },
5473
+ async close() {
5474
+ if (!pglite.closed) await pglite.close();
5475
+ }
5476
+ };
5477
+ } catch (_pgliteErr) {
5478
+ }
5386
5479
  let pgMemMod;
5387
5480
  let drizzlePg;
5388
5481
  try {
@@ -5391,12 +5484,11 @@ var MockDb = class {
5391
5484
  } catch {
5392
5485
  throw new NeetruDbError(
5393
5486
  "db_unavailable",
5394
- "[MockDb] sql() requer pg-mem + drizzle-orm + pg como devDependencies do consumer. Rode: npm install -D pg-mem drizzle-orm pg."
5487
+ "[MockDb] sql() requer @electric-sql/pglite OU pg-mem + drizzle-orm + pg como devDependencies. Preferido: npm install -D @electric-sql/pglite drizzle-orm. Alternativo: npm install -D pg-mem drizzle-orm pg."
5395
5488
  );
5396
5489
  }
5397
5490
  const memDb = pgMemMod.newDb({ noAstCoverageCheck: true });
5398
- if (options?.initSql) {
5399
- const ddls = Array.isArray(options.initSql) ? options.initSql : [options.initSql];
5491
+ if (ddls.length > 0) {
5400
5492
  for (const ddl of ddls) {
5401
5493
  memDb.public.none(ddl);
5402
5494
  }
@@ -5404,12 +5496,17 @@ var MockDb = class {
5404
5496
  const adapters = memDb.adapters.createPg();
5405
5497
  const PoolCtor = adapters.Pool;
5406
5498
  const pool = new PoolCtor();
5407
- const existingTypes = pool.types;
5408
- if (!existingTypes || typeof existingTypes.getTypeParser !== "function") {
5409
- pool.types = {
5410
- getTypeParser: () => (val) => val
5411
- };
5412
- }
5499
+ const origQuery = pool.query.bind(pool);
5500
+ pool.query = (q, ...rest) => {
5501
+ if (q !== null && typeof q === "object") {
5502
+ const queryObj = q;
5503
+ const types = queryObj["types"];
5504
+ if (types !== null && typeof types === "object" && "getTypeParser" in types) {
5505
+ delete types["getTypeParser"];
5506
+ }
5507
+ }
5508
+ return origQuery(q, ...rest);
5509
+ };
5413
5510
  const orm = drizzlePg.drizzle(pool, { schema });
5414
5511
  return {
5415
5512
  orm,
@@ -5699,6 +5796,7 @@ function createNeetruClient(config = {}) {
5699
5796
 
5700
5797
  // src/index.ts
5701
5798
  init_errors();
5799
+ init_db_errors();
5702
5800
  var VERSION = "2.0.0";
5703
5801
  function initNeetru(config) {
5704
5802
  const { apiUrl: _apiUrl, baseUrl, ...rest } = config;