@vandenberghinc/volt 1.2.3 → 1.2.4
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/backend/dist/cjs/backend/src/database/collection.d.ts +17 -10
- package/backend/dist/cjs/backend/src/database/collection.js +289 -126
- package/backend/dist/cjs/backend/src/payments/stripe/products.js +21 -6
- package/backend/dist/cjs/backend/src/stream.d.ts +58 -6
- package/backend/dist/cjs/backend/src/stream.js +91 -12
- package/backend/dist/cjs/backend/src/users.d.ts +11 -4
- package/backend/dist/cjs/backend/src/users.js +93 -26
- package/backend/dist/esm/backend/src/database/collection.d.ts +17 -10
- package/backend/dist/esm/backend/src/database/collection.js +311 -152
- package/backend/dist/esm/backend/src/database/filters/strict_update_filter_test.js +10 -0
- package/backend/dist/esm/backend/src/payments/stripe/products.js +24 -3
- package/backend/dist/esm/backend/src/stream.d.ts +58 -6
- package/backend/dist/esm/backend/src/stream.js +96 -12
- package/backend/dist/esm/backend/src/users.d.ts +11 -4
- package/backend/dist/esm/backend/src/users.js +95 -27
- package/frontend/dist/backend/src/database/collection.d.ts +17 -10
- package/frontend/dist/backend/src/database/collection.js +311 -152
- package/frontend/dist/backend/src/payments/stripe/products.js +24 -3
- package/frontend/dist/backend/src/stream.d.ts +58 -6
- package/frontend/dist/backend/src/stream.js +96 -12
- package/frontend/dist/backend/src/users.d.ts +11 -4
- package/frontend/dist/backend/src/users.js +95 -27
- package/frontend/dist/frontend/src/modules/user.js +3 -2
- package/frontend/dist/frontend/src/ui/table.d.ts +2 -2
- package/frontend/dist/frontend/src/ui/table.js +3 -6
- package/package.json +1 -1
|
@@ -492,6 +492,314 @@ export class Collection {
|
|
|
492
492
|
_is_operator_update_or_pipeline(operation) {
|
|
493
493
|
return Array.isArray(operation) || (operation && typeof operation === "object" && Object.keys(operation).some(k => k[0] === "$"));
|
|
494
494
|
}
|
|
495
|
+
_index_key_signature(keys) {
|
|
496
|
+
// Preserve order (Mongo treats order as significant for compound indexes)
|
|
497
|
+
return Object.entries(keys).map(([k, v]) => `${k}:${v}`).join("|");
|
|
498
|
+
}
|
|
499
|
+
_keys_equal(a, b) {
|
|
500
|
+
const aEnt = Object.entries(a);
|
|
501
|
+
const bEnt = Object.entries(b);
|
|
502
|
+
if (aEnt.length !== bEnt.length)
|
|
503
|
+
return false;
|
|
504
|
+
for (let i = 0; i < aEnt.length; i++) {
|
|
505
|
+
const [ak, av] = aEnt[i];
|
|
506
|
+
const [bk, bv] = bEnt[i];
|
|
507
|
+
if (ak !== bk || av !== bv)
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
_normalize_index_opts(opts) {
|
|
513
|
+
// ---- Normalize inputs ----
|
|
514
|
+
let key;
|
|
515
|
+
let keys;
|
|
516
|
+
let options;
|
|
517
|
+
let unique;
|
|
518
|
+
let sparse;
|
|
519
|
+
let forced = false;
|
|
520
|
+
if (typeof opts === "string") {
|
|
521
|
+
key = opts;
|
|
522
|
+
unique = undefined;
|
|
523
|
+
sparse = undefined;
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
({ key, keys, forced = false } = opts);
|
|
527
|
+
const user_options = opts.options;
|
|
528
|
+
options = user_options ? { ...user_options } : undefined;
|
|
529
|
+
// Conflict guard between `unique` and `options.unique`
|
|
530
|
+
if (opts.unique != null && options?.unique != null && opts.unique !== options.unique) {
|
|
531
|
+
throw new InvalidUsageError({
|
|
532
|
+
message: `Encountered different values for attribute 'unique': ${opts.unique} and 'options.unique': ${options.unique}.`,
|
|
533
|
+
reason: "invalid_unique_option",
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
unique = opts.unique ?? options?.unique;
|
|
537
|
+
// Conflict guard between `sparse` and `options.sparse`
|
|
538
|
+
if (opts.sparse != null && options?.sparse != null && opts.sparse !== options.sparse) {
|
|
539
|
+
throw new InvalidUsageError({
|
|
540
|
+
message: `Encountered different values for attribute 'sparse': ${opts.sparse} and 'options.sparse': ${options.sparse}.`,
|
|
541
|
+
reason: "invalid_sparse_option",
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
sparse = opts.sparse ?? options?.sparse;
|
|
545
|
+
}
|
|
546
|
+
// Ensure `unique`/`sparse` are reflected in `options`
|
|
547
|
+
if (unique != null) {
|
|
548
|
+
options = options || {};
|
|
549
|
+
options.unique = unique;
|
|
550
|
+
}
|
|
551
|
+
if (sparse != null) {
|
|
552
|
+
options = options || {};
|
|
553
|
+
options.sparse = sparse;
|
|
554
|
+
}
|
|
555
|
+
// ---- Build keys object (same rules everywhere) ----
|
|
556
|
+
let keys_obj;
|
|
557
|
+
if (key) {
|
|
558
|
+
keys_obj = { [key]: 1 };
|
|
559
|
+
}
|
|
560
|
+
else if (Array.isArray(keys) && keys.length > 0) {
|
|
561
|
+
keys_obj = {};
|
|
562
|
+
for (const k of keys)
|
|
563
|
+
keys_obj[k] = 1;
|
|
564
|
+
}
|
|
565
|
+
else if (keys != null && typeof keys === "object") {
|
|
566
|
+
keys_obj = keys;
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
throw new InvalidUsageError({
|
|
570
|
+
message: "Define one of the following parameters: [key, keys].",
|
|
571
|
+
reason: "invalid_index_definition",
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
return { keys_obj, options, forced };
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Drop all indexes that are NOT part of this._init_indexes, excluding _id_ (and TTL index if enabled).
|
|
578
|
+
*
|
|
579
|
+
* @note We match by key pattern rather than name because names can differ.
|
|
580
|
+
*/
|
|
581
|
+
async _drop_non_init_indexes() {
|
|
582
|
+
this.assert_not_transaction_based();
|
|
583
|
+
this.assert_init();
|
|
584
|
+
const existing = await this._col.listIndexes().toArray();
|
|
585
|
+
// Desired signatures (same parsing as create_index)
|
|
586
|
+
const desired = new Set();
|
|
587
|
+
for (const item of (this._init_indexes ?? [])) {
|
|
588
|
+
const { keys_obj } = this._normalize_index_opts(item);
|
|
589
|
+
desired.add(this._index_key_signature(keys_obj));
|
|
590
|
+
}
|
|
591
|
+
// Always keep _id_
|
|
592
|
+
const protected_names = new Set(["_id_"]);
|
|
593
|
+
// Keep TTL index if TTL is enabled (managed by _setup_ttl)
|
|
594
|
+
const keep_ttl = this.ttl_enabled;
|
|
595
|
+
const ttl_sig = this._index_key_signature({ __ttl_timestamp: 1 });
|
|
596
|
+
for (const ix of existing) {
|
|
597
|
+
const name = ix?.name;
|
|
598
|
+
if (!name)
|
|
599
|
+
continue;
|
|
600
|
+
if (protected_names.has(name))
|
|
601
|
+
continue;
|
|
602
|
+
const keyObj = ix.key;
|
|
603
|
+
if (!keyObj)
|
|
604
|
+
continue;
|
|
605
|
+
const sig = this._index_key_signature(keyObj);
|
|
606
|
+
if (desired.has(sig))
|
|
607
|
+
continue;
|
|
608
|
+
if (keep_ttl && sig === ttl_sig)
|
|
609
|
+
continue;
|
|
610
|
+
try {
|
|
611
|
+
this.db.server.log(3, `Dropping stale index "${name}" on collection: ${this.name}`);
|
|
612
|
+
await this._col.dropIndex(name);
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
if (err?.codeName !== "IndexNotFound")
|
|
616
|
+
throw err;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Creates indexes on collections.
|
|
622
|
+
*
|
|
623
|
+
* @note When transaction mode is enabled, the session option will not be used.
|
|
624
|
+
*
|
|
625
|
+
* @param opts The index create options.
|
|
626
|
+
*/
|
|
627
|
+
async _create_index(opts) {
|
|
628
|
+
this.assert_not_transaction_based();
|
|
629
|
+
if (!this.initialized) {
|
|
630
|
+
await this.init();
|
|
631
|
+
}
|
|
632
|
+
this.assert_init();
|
|
633
|
+
const { keys_obj, options, forced } = this._normalize_index_opts(opts);
|
|
634
|
+
const drop_index = async () => {
|
|
635
|
+
const existing = await this._col.listIndexes().toArray();
|
|
636
|
+
const match = existing.find(ix => {
|
|
637
|
+
const ix_key = ix?.key;
|
|
638
|
+
if (!ix_key)
|
|
639
|
+
return false;
|
|
640
|
+
return this._keys_equal(ix_key, keys_obj);
|
|
641
|
+
});
|
|
642
|
+
if (match?.name) {
|
|
643
|
+
try {
|
|
644
|
+
await this._col.dropIndex(match.name);
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
if (err?.codeName !== "IndexNotFound")
|
|
648
|
+
throw err;
|
|
649
|
+
}
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
// fallback: name provided in options
|
|
653
|
+
if (options?.name) {
|
|
654
|
+
try {
|
|
655
|
+
await this._col.dropIndex(options.name);
|
|
656
|
+
}
|
|
657
|
+
catch (err) {
|
|
658
|
+
if (err?.codeName !== "IndexNotFound")
|
|
659
|
+
throw err;
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
// last resort synthesized (may not match Mongo’s generated name for compound cases)
|
|
664
|
+
const synthesized = Object.entries(keys_obj).map(([k, v]) => `${k}_${v}`).join("_");
|
|
665
|
+
try {
|
|
666
|
+
await this._col.dropIndex(synthesized);
|
|
667
|
+
}
|
|
668
|
+
catch (err) {
|
|
669
|
+
if (err?.codeName !== "IndexNotFound")
|
|
670
|
+
throw err;
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
try {
|
|
674
|
+
try {
|
|
675
|
+
return await this._col.createIndex(keys_obj, options);
|
|
676
|
+
}
|
|
677
|
+
catch (err) {
|
|
678
|
+
if (forced && err && typeof err === "object" && err.codeName === "IndexKeySpecsConflict") {
|
|
679
|
+
await drop_index();
|
|
680
|
+
return await this._col.createIndex(keys_obj, options);
|
|
681
|
+
}
|
|
682
|
+
throw err;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
catch (err) {
|
|
686
|
+
throw new Error(`Failed to create index on collection "${this.name}": ${err}`, { cause: err });
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
// async create_index(opts: string | Collection.IndexOpts): Promise<string> {
|
|
690
|
+
// // Not supported on transaction-based collections.
|
|
691
|
+
// this.assert_not_transaction_based();
|
|
692
|
+
// // Ensure initialized
|
|
693
|
+
// if (!this.initialized) { await this.init(); } this.assert_init();
|
|
694
|
+
// // ---- Normalize inputs ----
|
|
695
|
+
// let key: string | undefined;
|
|
696
|
+
// let keys: string[] | Record<string, number> | undefined;
|
|
697
|
+
// let options: mongodb.CreateIndexesOptions | undefined;
|
|
698
|
+
// let unique: boolean | undefined;
|
|
699
|
+
// let sparse: boolean | undefined;
|
|
700
|
+
// let forced = false;
|
|
701
|
+
// if (typeof opts === "string") {
|
|
702
|
+
// key = opts;
|
|
703
|
+
// unique = undefined;
|
|
704
|
+
// sparse = undefined;
|
|
705
|
+
// } else {
|
|
706
|
+
// ({ key, keys, forced = false } = opts);
|
|
707
|
+
// const options = opts.options as unknown as undefined | mongodb.CreateIndexesOptions;
|
|
708
|
+
// // Conflict guard between `unique` and `options.unique`
|
|
709
|
+
// if (opts.unique != null && options?.unique != null && opts.unique !== options.unique) {
|
|
710
|
+
// throw new InvalidUsageError({
|
|
711
|
+
// message: `Encountered different values for attribute 'unique': ${opts.unique} and 'options.unique': ${options.unique}.`,
|
|
712
|
+
// reason: "invalid_unique_option",
|
|
713
|
+
// });
|
|
714
|
+
// }
|
|
715
|
+
// unique = opts.unique ?? options?.unique;
|
|
716
|
+
// // Conflict guard between `sparse` and `options.sparse`
|
|
717
|
+
// if (opts.sparse != null && options?.sparse != null && opts.sparse !== options.sparse) {
|
|
718
|
+
// throw new InvalidUsageError({
|
|
719
|
+
// message: `Encountered different values for attribute 'sparse': ${opts.sparse} and 'options.sparse': ${options.sparse}.`,
|
|
720
|
+
// reason: "invalid_sparse_option",
|
|
721
|
+
// });
|
|
722
|
+
// }
|
|
723
|
+
// sparse = opts.sparse ?? options?.sparse;
|
|
724
|
+
// }
|
|
725
|
+
// // Ensure `unique` in options when provided
|
|
726
|
+
// if (unique) {
|
|
727
|
+
// options = options || {};
|
|
728
|
+
// options.unique = unique;
|
|
729
|
+
// }
|
|
730
|
+
// // Ensure `sparse` in options when provided
|
|
731
|
+
// if (sparse) {
|
|
732
|
+
// options = options || {};
|
|
733
|
+
// options.sparse = sparse;
|
|
734
|
+
// }
|
|
735
|
+
// // Build keys object
|
|
736
|
+
// let keys_obj: Record<string, number>;
|
|
737
|
+
// if (key) {
|
|
738
|
+
// keys_obj = { [key]: 1 };
|
|
739
|
+
// } else if (Array.isArray(keys) && keys.length > 0) {
|
|
740
|
+
// keys_obj = {};
|
|
741
|
+
// for (const k of keys) keys_obj[k] = 1;
|
|
742
|
+
// } else if (keys != null && typeof keys === "object") {
|
|
743
|
+
// keys_obj = keys as Record<string, number>;
|
|
744
|
+
// } else {
|
|
745
|
+
// throw new InvalidUsageError({
|
|
746
|
+
// message: "Define one of the following parameters: [key, keys].",
|
|
747
|
+
// reason: "invalid_index_definition",
|
|
748
|
+
// });
|
|
749
|
+
// }
|
|
750
|
+
// const drop_index = async () => {
|
|
751
|
+
// try {
|
|
752
|
+
// const existing = await this._col.listIndexes().toArray();
|
|
753
|
+
// const match = existing.find(ix => {
|
|
754
|
+
// const ix_key = ix?.key as Record<string, number> | undefined;
|
|
755
|
+
// if (!ix_key) return false;
|
|
756
|
+
// const a = Object.entries(ix_key);
|
|
757
|
+
// const b = Object.entries(keys_obj);
|
|
758
|
+
// if (a.length !== b.length) return false;
|
|
759
|
+
// // exact key-value equality (order-insensitive)
|
|
760
|
+
// const as = new Map(a);
|
|
761
|
+
// for (const [kk, vv] of b) {
|
|
762
|
+
// if (as.get(kk) !== vv) return false;
|
|
763
|
+
// }
|
|
764
|
+
// return true;
|
|
765
|
+
// });
|
|
766
|
+
// // Prefer matched key's real name
|
|
767
|
+
// if (match?.name) {
|
|
768
|
+
// try { await this._col.dropIndex(match.name); }
|
|
769
|
+
// catch (err: any) { if (err?.codeName !== "IndexNotFound") throw err; }
|
|
770
|
+
// } else if (options?.name) {
|
|
771
|
+
// try { await this._col.dropIndex(options.name); }
|
|
772
|
+
// catch (err: any) { if (err?.codeName !== "IndexNotFound") throw err; }
|
|
773
|
+
// } else {
|
|
774
|
+
// // last-resort synthesized name (simple cases)
|
|
775
|
+
// const synthesized = Object.entries(keys_obj).map(([k, v]) => `${k}_${v}`).join("_");
|
|
776
|
+
// try { await this._col.dropIndex(synthesized); }
|
|
777
|
+
// catch (err: any) { if (err?.codeName !== "IndexNotFound") throw err; }
|
|
778
|
+
// }
|
|
779
|
+
// } catch (err) {
|
|
780
|
+
// // If listIndexes itself fails for some reason, do not hide the error
|
|
781
|
+
// throw new Error(`Failed to create index on collection "${this.name}": ${err}`, { cause: err });
|
|
782
|
+
// }
|
|
783
|
+
// }
|
|
784
|
+
// try {
|
|
785
|
+
// // Create (or re-create)
|
|
786
|
+
// try {
|
|
787
|
+
// return await this._col.createIndex(keys_obj, options);
|
|
788
|
+
// }
|
|
789
|
+
// // Retry once on IndexKeySpecsConflict when forced=true
|
|
790
|
+
// catch (err) {
|
|
791
|
+
// if (forced && err && typeof err === "object" && (
|
|
792
|
+
// (err as any).codeName === "IndexKeySpecsConflict"
|
|
793
|
+
// )) {
|
|
794
|
+
// await drop_index();
|
|
795
|
+
// return await this._col.createIndex(keys_obj, options);
|
|
796
|
+
// }
|
|
797
|
+
// throw err;
|
|
798
|
+
// }
|
|
799
|
+
// } catch (err) {
|
|
800
|
+
// throw new Error(`Failed to create index on collection "${this.name}": ${err}`, { cause: err });
|
|
801
|
+
// }
|
|
802
|
+
// }
|
|
495
803
|
// -------------------------------------------------------------------
|
|
496
804
|
// Public methods.
|
|
497
805
|
// -------------------------------------------------------------------
|
|
@@ -567,11 +875,13 @@ export class Collection {
|
|
|
567
875
|
this.db.server.log(3, "Setting up TTL index for collection: " + this.name);
|
|
568
876
|
await this._setup_ttl();
|
|
569
877
|
}
|
|
878
|
+
// Drop indexes that are not in this._init_indexes (keep _id_ + TTL).
|
|
879
|
+
await this._drop_non_init_indexes();
|
|
570
880
|
// Create indexes.
|
|
571
881
|
if (this._init_indexes?.length) {
|
|
572
882
|
for (const item of this._init_indexes) {
|
|
573
883
|
this.db.server.log(3, "Creating index " + JSON.stringify(item) + " on collection: " + this.name);
|
|
574
|
-
await this.
|
|
884
|
+
await this._create_index(item);
|
|
575
885
|
}
|
|
576
886
|
}
|
|
577
887
|
}
|
|
@@ -686,157 +996,6 @@ export class Collection {
|
|
|
686
996
|
// No need to pass session obj here.
|
|
687
997
|
return (await this._col.listIndexes().toArray()).some(x => x.name === index);
|
|
688
998
|
}
|
|
689
|
-
/**
|
|
690
|
-
* Creates indexes on collections.
|
|
691
|
-
*
|
|
692
|
-
* @note When transaction mode is enabled, the session option will not be used.
|
|
693
|
-
*
|
|
694
|
-
* @param opts The index create options.
|
|
695
|
-
*
|
|
696
|
-
* @docs
|
|
697
|
-
*/
|
|
698
|
-
async create_index(opts) {
|
|
699
|
-
// Not supported on transaction-based collections.
|
|
700
|
-
this.assert_not_transaction_based();
|
|
701
|
-
// Ensure initialized
|
|
702
|
-
if (!this.initialized) {
|
|
703
|
-
await this.init();
|
|
704
|
-
}
|
|
705
|
-
this.assert_init();
|
|
706
|
-
// ---- Normalize inputs ----
|
|
707
|
-
let key;
|
|
708
|
-
let keys;
|
|
709
|
-
let options;
|
|
710
|
-
let unique;
|
|
711
|
-
let sparse;
|
|
712
|
-
let forced = false;
|
|
713
|
-
if (typeof opts === "string") {
|
|
714
|
-
key = opts;
|
|
715
|
-
unique = undefined;
|
|
716
|
-
sparse = undefined;
|
|
717
|
-
}
|
|
718
|
-
else {
|
|
719
|
-
({ key, keys, forced = false } = opts);
|
|
720
|
-
const options = opts.options;
|
|
721
|
-
// Conflict guard between `unique` and `options.unique`
|
|
722
|
-
if (opts.unique != null && options?.unique != null && opts.unique !== options.unique) {
|
|
723
|
-
throw new InvalidUsageError({
|
|
724
|
-
message: `Encountered different values for attribute 'unique': ${opts.unique} and 'options.unique': ${options.unique}.`,
|
|
725
|
-
reason: "invalid_unique_option",
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
unique = opts.unique ?? options?.unique;
|
|
729
|
-
// Conflict guard between `sparse` and `options.sparse`
|
|
730
|
-
if (opts.sparse != null && options?.sparse != null && opts.sparse !== options.sparse) {
|
|
731
|
-
throw new InvalidUsageError({
|
|
732
|
-
message: `Encountered different values for attribute 'sparse': ${opts.sparse} and 'options.sparse': ${options.sparse}.`,
|
|
733
|
-
reason: "invalid_sparse_option",
|
|
734
|
-
});
|
|
735
|
-
}
|
|
736
|
-
sparse = opts.sparse ?? options?.sparse;
|
|
737
|
-
}
|
|
738
|
-
// Ensure `unique` in options when provided
|
|
739
|
-
if (unique) {
|
|
740
|
-
options = options || {};
|
|
741
|
-
options.unique = unique;
|
|
742
|
-
}
|
|
743
|
-
// Ensure `sparse` in options when provided
|
|
744
|
-
if (sparse) {
|
|
745
|
-
options = options || {};
|
|
746
|
-
options.sparse = sparse;
|
|
747
|
-
}
|
|
748
|
-
// Build keys object
|
|
749
|
-
let keys_obj;
|
|
750
|
-
if (key) {
|
|
751
|
-
keys_obj = { [key]: 1 };
|
|
752
|
-
}
|
|
753
|
-
else if (Array.isArray(keys) && keys.length > 0) {
|
|
754
|
-
keys_obj = {};
|
|
755
|
-
for (const k of keys)
|
|
756
|
-
keys_obj[k] = 1;
|
|
757
|
-
}
|
|
758
|
-
else if (keys != null && typeof keys === "object") {
|
|
759
|
-
keys_obj = keys;
|
|
760
|
-
}
|
|
761
|
-
else {
|
|
762
|
-
throw new InvalidUsageError({
|
|
763
|
-
message: "Define one of the following parameters: [key, keys].",
|
|
764
|
-
reason: "invalid_index_definition",
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
const drop_index = async () => {
|
|
768
|
-
try {
|
|
769
|
-
const existing = await this._col.listIndexes().toArray();
|
|
770
|
-
const match = existing.find(ix => {
|
|
771
|
-
const ix_key = ix?.key;
|
|
772
|
-
if (!ix_key)
|
|
773
|
-
return false;
|
|
774
|
-
const a = Object.entries(ix_key);
|
|
775
|
-
const b = Object.entries(keys_obj);
|
|
776
|
-
if (a.length !== b.length)
|
|
777
|
-
return false;
|
|
778
|
-
// exact key-value equality (order-insensitive)
|
|
779
|
-
const as = new Map(a);
|
|
780
|
-
for (const [kk, vv] of b) {
|
|
781
|
-
if (as.get(kk) !== vv)
|
|
782
|
-
return false;
|
|
783
|
-
}
|
|
784
|
-
return true;
|
|
785
|
-
});
|
|
786
|
-
// Prefer matched key's real name
|
|
787
|
-
if (match?.name) {
|
|
788
|
-
try {
|
|
789
|
-
await this._col.dropIndex(match.name);
|
|
790
|
-
}
|
|
791
|
-
catch (err) {
|
|
792
|
-
if (err?.codeName !== "IndexNotFound")
|
|
793
|
-
throw err;
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
else if (options?.name) {
|
|
797
|
-
try {
|
|
798
|
-
await this._col.dropIndex(options.name);
|
|
799
|
-
}
|
|
800
|
-
catch (err) {
|
|
801
|
-
if (err?.codeName !== "IndexNotFound")
|
|
802
|
-
throw err;
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
else {
|
|
806
|
-
// last-resort synthesized name (simple cases)
|
|
807
|
-
const synthesized = Object.entries(keys_obj).map(([k, v]) => `${k}_${v}`).join("_");
|
|
808
|
-
try {
|
|
809
|
-
await this._col.dropIndex(synthesized);
|
|
810
|
-
}
|
|
811
|
-
catch (err) {
|
|
812
|
-
if (err?.codeName !== "IndexNotFound")
|
|
813
|
-
throw err;
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
catch (err) {
|
|
818
|
-
// If listIndexes itself fails for some reason, do not hide the error
|
|
819
|
-
throw new Error(`Failed to create index on collection "${this.name}": ${err}`, { cause: err });
|
|
820
|
-
}
|
|
821
|
-
};
|
|
822
|
-
try {
|
|
823
|
-
// Create (or re-create)
|
|
824
|
-
try {
|
|
825
|
-
return await this._col.createIndex(keys_obj, options);
|
|
826
|
-
}
|
|
827
|
-
// Retry once on IndexKeySpecsConflict when forced=true
|
|
828
|
-
catch (err) {
|
|
829
|
-
if (forced && err && typeof err === "object" && (err.codeName === "IndexKeySpecsConflict")) {
|
|
830
|
-
await drop_index();
|
|
831
|
-
return await this._col.createIndex(keys_obj, options);
|
|
832
|
-
}
|
|
833
|
-
throw err;
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
catch (err) {
|
|
837
|
-
throw new Error(`Failed to create index on collection "${this.name}": ${err}`, { cause: err });
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
999
|
/**
|
|
841
1000
|
* Standalone helper: merge `source` into `target` for missing keys only.
|
|
842
1001
|
* Clones assigned nested objects/arrays/dates once (when `clone` is true).
|
|
@@ -224,6 +224,7 @@ async function list_all_stripe_products(client) {
|
|
|
224
224
|
const page = await stripe_api_call(() => client.products.list({
|
|
225
225
|
limit: stripe_list_page_size,
|
|
226
226
|
starting_after,
|
|
227
|
+
expand: ["data.default_price"],
|
|
227
228
|
}), { operation: "products.list_all", starting_after });
|
|
228
229
|
all_products.push(...page.data);
|
|
229
230
|
if (!page.has_more || page.data.length === 0) {
|
|
@@ -408,6 +409,7 @@ async function create_stripe_product(client, server, product) {
|
|
|
408
409
|
metadata: {
|
|
409
410
|
[app_product_id_metadata_key]: product.id,
|
|
410
411
|
},
|
|
412
|
+
expand: ["default_price"],
|
|
411
413
|
}, { idempotencyKey: generate_random_idempotency_key(`create_product_${product.id}`) }), { operation: "products.create", app_product_id: product.id });
|
|
412
414
|
}
|
|
413
415
|
/**
|
|
@@ -433,13 +435,32 @@ async function update_stripe_product_if_needed(client, server, stripe_product, p
|
|
|
433
435
|
description: product.description,
|
|
434
436
|
tax_code: product.tax_code,
|
|
435
437
|
images: product.images,
|
|
438
|
+
expand: ["default_price"],
|
|
436
439
|
}, { idempotencyKey: generate_random_idempotency_key(`update_product_${product.id}_${stripe_product.id}`) }), { operation: "products.update", app_product_id: product.id, stripe_product_id: stripe_product.id });
|
|
437
440
|
}
|
|
438
441
|
/**
|
|
439
442
|
* Update default price on a stripe product if needed.
|
|
440
443
|
*/
|
|
441
|
-
async function update_stripe_product_default_price_if_needed(client, server, stripe_product, default_price) {
|
|
442
|
-
|
|
444
|
+
async function update_stripe_product_default_price_if_needed(client, server, stripe_product, default_price, other_plans_from_parent_subscription) {
|
|
445
|
+
// Extract default price.
|
|
446
|
+
let default_price_id = null;
|
|
447
|
+
if (typeof stripe_product.default_price === "string") {
|
|
448
|
+
default_price_id = stripe_product.default_price;
|
|
449
|
+
}
|
|
450
|
+
else if (stripe_product.default_price && typeof stripe_product.default_price === "object") {
|
|
451
|
+
default_price_id = stripe_product.default_price.id;
|
|
452
|
+
}
|
|
453
|
+
// If its still undefined, fetch the product to get the default_price expanded (this should be rare since we expand it on list/create/update).
|
|
454
|
+
if (!default_price_id) {
|
|
455
|
+
const fetched = await stripe_api_call(() => client.products.retrieve(stripe_product.id, {
|
|
456
|
+
expand: ["default_price"],
|
|
457
|
+
}), { operation: "products.retrieve_for_default_price", stripe_product_id: stripe_product.id });
|
|
458
|
+
default_price_id = typeof fetched.default_price === "string" ? fetched.default_price : fetched.default_price?.id ?? null;
|
|
459
|
+
}
|
|
460
|
+
// If the default price is already correct, do nothing.
|
|
461
|
+
if (default_price_id === default_price.id
|
|
462
|
+
// For subscription products, the default_price may be shared across multiple plans, so we also check if any other plan from the same subscription is using the price. If so, we should not update the default_price since it would affect those plans as well.
|
|
463
|
+
|| other_plans_from_parent_subscription?.some((plan) => plan.stripe_price_id === default_price_id)) {
|
|
443
464
|
return;
|
|
444
465
|
}
|
|
445
466
|
server.log(0, `Updating default price for Stripe product '${stripe_product.id}' to price '${default_price.id}'`);
|
|
@@ -756,7 +777,7 @@ async function initialize_product(client, server, product, stripe_products_by_ap
|
|
|
756
777
|
const updated_prices = [...active_prices, stripe_price];
|
|
757
778
|
active_prices_by_stripe_product_id.set(stripe_product.id, updated_prices);
|
|
758
779
|
}
|
|
759
|
-
await update_stripe_product_default_price_if_needed(client, server, stripe_product, stripe_price);
|
|
780
|
+
await update_stripe_product_default_price_if_needed(client, server, stripe_product, stripe_price, initialized_plans);
|
|
760
781
|
initialized_plans.push({
|
|
761
782
|
...plan,
|
|
762
783
|
type: "subscription_plan",
|
|
@@ -362,17 +362,69 @@ export declare class Stream {
|
|
|
362
362
|
*/
|
|
363
363
|
remove_headers(...names: string[]): this;
|
|
364
364
|
/**
|
|
365
|
-
* Set a cookie
|
|
365
|
+
* Set a cookie to be sent with the response.
|
|
366
|
+
*
|
|
367
|
+
* Accepts either:
|
|
368
|
+
* 1) a pre-built cookie header string (used as-is, no validation), or
|
|
369
|
+
* 2) a structured object describing the cookie, from which a standards-compliant
|
|
370
|
+
* cookie string will be generated.
|
|
371
|
+
*
|
|
372
|
+
* If a cookie with the same name already exists in the pending response list,
|
|
373
|
+
* it will be replaced.
|
|
374
|
+
*
|
|
375
|
+
* @warning Cookies are only included in the response when using `send()`,
|
|
376
|
+
* `success()` or `error()`.
|
|
366
377
|
*
|
|
367
|
-
* @warning Will only be added to the response when the user uses `send()`, `success()` or `error()`.
|
|
368
|
-
* @param cookie The cookie string.
|
|
369
378
|
* @example
|
|
370
379
|
* ```ts
|
|
371
|
-
* stream.set_cookie("
|
|
380
|
+
* stream.set_cookie("sid=abc123; Path=/; SameSite=Lax; Secure; HttpOnly");
|
|
381
|
+
*
|
|
382
|
+
* stream.set_cookie({
|
|
383
|
+
* name: "sid",
|
|
384
|
+
* value: session_id,
|
|
385
|
+
* http_only: true,
|
|
386
|
+
* secure: true,
|
|
387
|
+
* same_site: "Lax",
|
|
388
|
+
* path: "/",
|
|
389
|
+
* max_age: 60 * 60 * 24 * 14,
|
|
390
|
+
* });
|
|
372
391
|
* ```
|
|
373
|
-
* @docs
|
|
374
392
|
*/
|
|
375
|
-
set_cookie(cookie: string
|
|
393
|
+
set_cookie(cookie: string | {
|
|
394
|
+
/** Cookie name (required). */
|
|
395
|
+
name: string;
|
|
396
|
+
/** Cookie value. Will be URI-encoded. Defaults to empty string. */
|
|
397
|
+
value?: string | number | boolean | null;
|
|
398
|
+
/** Cookie path attribute. Defaults to "/". */
|
|
399
|
+
path?: string;
|
|
400
|
+
/** Cookie domain attribute. */
|
|
401
|
+
domain?: string;
|
|
402
|
+
/** Max-Age in seconds. Must be a finite number. */
|
|
403
|
+
max_age?: number;
|
|
404
|
+
/** Expiration date (Date or preformatted HTTP date string). */
|
|
405
|
+
expires?: Date | string;
|
|
406
|
+
/** Adds the Secure attribute (HTTPS only). */
|
|
407
|
+
secure?: boolean;
|
|
408
|
+
/** Adds the HttpOnly attribute (not accessible to JS). */
|
|
409
|
+
http_only?: boolean;
|
|
410
|
+
/**
|
|
411
|
+
* SameSite attribute.
|
|
412
|
+
* Use "Lax" for most session cookies.
|
|
413
|
+
* Use "None" only together with `secure: true`.
|
|
414
|
+
*/
|
|
415
|
+
same_site?: "Strict" | "Lax" | "None";
|
|
416
|
+
/**
|
|
417
|
+
* Cookie name prefix.
|
|
418
|
+
* "__Host-" requires: secure=true, path="/", and no domain.
|
|
419
|
+
* "__Secure-" requires: secure=true.
|
|
420
|
+
*/
|
|
421
|
+
prefix?: "__Host-" | "__Secure-";
|
|
422
|
+
/**
|
|
423
|
+
* Additional raw attributes appended verbatim.
|
|
424
|
+
* Example: ["Priority=High"]
|
|
425
|
+
*/
|
|
426
|
+
extra?: string[];
|
|
427
|
+
}): this;
|
|
376
428
|
/**
|
|
377
429
|
* Set cookies that will be sent with the response.
|
|
378
430
|
*
|