@proteinjs/db 1.43.0 → 1.45.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/generated/index.js +1 -1
  3. package/dist/generated/index.js.map +1 -1
  4. package/dist/generated/test/index.d.ts.map +1 -1
  5. package/dist/generated/test/index.js +3 -1
  6. package/dist/generated/test/index.js.map +1 -1
  7. package/dist/src/Table.d.ts +37 -1
  8. package/dist/src/Table.d.ts.map +1 -1
  9. package/dist/src/Table.js.map +1 -1
  10. package/dist/src/auth/TableAuth.d.ts +11 -1
  11. package/dist/src/auth/TableAuth.d.ts.map +1 -1
  12. package/dist/src/auth/TableAuth.js +39 -13
  13. package/dist/src/auth/TableAuth.js.map +1 -1
  14. package/dist/src/source/SourceRecord.d.ts +2 -3
  15. package/dist/src/source/SourceRecord.d.ts.map +1 -1
  16. package/dist/src/source/SourceRecord.js +16 -2
  17. package/dist/src/source/SourceRecord.js.map +1 -1
  18. package/dist/src/source/SourceRecordLoader.d.ts +12 -0
  19. package/dist/src/source/SourceRecordLoader.d.ts.map +1 -1
  20. package/dist/src/source/SourceRecordLoader.js +72 -50
  21. package/dist/src/source/SourceRecordLoader.js.map +1 -1
  22. package/dist/src/tables/MigrationTable.d.ts +22 -4
  23. package/dist/src/tables/MigrationTable.d.ts.map +1 -1
  24. package/dist/src/tables/MigrationTable.js +38 -16
  25. package/dist/src/tables/MigrationTable.js.map +1 -1
  26. package/dist/test/SourceRecordLoader.test.js +151 -0
  27. package/dist/test/SourceRecordLoader.test.js.map +1 -1
  28. package/dist/test/TableAuthIdentity.test.d.ts +2 -0
  29. package/dist/test/TableAuthIdentity.test.d.ts.map +1 -0
  30. package/dist/test/TableAuthIdentity.test.js +110 -0
  31. package/dist/test/TableAuthIdentity.test.js.map +1 -0
  32. package/dist/test/reusable/SourceRecordSyncTests.d.ts.map +1 -1
  33. package/dist/test/reusable/SourceRecordSyncTests.js +124 -0
  34. package/dist/test/reusable/SourceRecordSyncTests.js.map +1 -1
  35. package/dist/test/util/tables/sourceRecordSyncTestTables.d.ts +17 -0
  36. package/dist/test/util/tables/sourceRecordSyncTestTables.d.ts.map +1 -1
  37. package/dist/test/util/tables/sourceRecordSyncTestTables.js +26 -1
  38. package/dist/test/util/tables/sourceRecordSyncTestTables.js.map +1 -1
  39. package/generated/index.ts +1 -1
  40. package/generated/test/index.ts +3 -1
  41. package/package.json +2 -2
  42. package/src/Table.ts +38 -1
  43. package/src/auth/TableAuth.ts +45 -20
  44. package/src/source/SourceRecord.ts +17 -4
  45. package/src/source/SourceRecordLoader.ts +30 -3
  46. package/src/tables/MigrationTable.ts +43 -16
  47. package/test/SourceRecordLoader.test.ts +90 -0
  48. package/test/TableAuthIdentity.test.ts +115 -0
  49. package/test/reusable/SourceRecordSyncTests.ts +72 -0
  50. package/test/util/tables/sourceRecordSyncTestTables.ts +25 -0
@@ -149,6 +149,7 @@ export const sourceRecordSyncTests = (
149
149
  await db.delete(machineTable, {});
150
150
  await db.delete(defaultPolicyTable, {});
151
151
  await db.delete(sourceRecordSyncTestTables.InheritedStamp, {});
152
+ await db.delete(sourceRecordSyncTestTables.SyncDerivedName, {});
152
153
  RecordingMachineAccountWatcher.updates = [];
153
154
  });
154
155
 
@@ -729,6 +730,77 @@ export const sourceRecordSyncTests = (
729
730
  });
730
731
  });
731
732
 
733
+ describe('stamp hygiene: an unchanged declaration is never rewritten', () => {
734
+ const versioned = (displayName: string) => ({
735
+ table: defaultPolicyTable,
736
+ record: { id: 'v-1', email: 'v1@test.local', displayName },
737
+ });
738
+
739
+ test('a version bump alone re-stamps nothing: the next release leaves unchanged rows (and their updated stamp) alone', async () => {
740
+ const db = getDbAsSystem();
741
+ await bootAsSource('@test/pkg-a', [versioned('same')], '1.23.0');
742
+ const stamped = await db.get(defaultPolicyTable, { id: 'v-1' });
743
+ expect(stamped.sourcePackageVersion).toBe('1.23.0');
744
+
745
+ // The next release: the SAME declaration under a newer package version (every release
746
+ // bumps the declaring package). Pre-fix this counted the version stamp as drift and
747
+ // rewrote every row on every boot — `updated` churned on rows nobody changed.
748
+ const summary = await bootAsSource('@test/pkg-a', [versioned('same')], '1.24.0');
749
+ expect(summary[defaultPolicyTable.name]).toMatchObject({ updates: 0, unchanged: 1 });
750
+ const idle = await db.get(defaultPolicyTable, { id: 'v-1' });
751
+ expect(idle.updated.valueOf()).toBe(stamped.updated.valueOf());
752
+ // The stamp records the version that last WROTE the row's definition.
753
+ expect(idle.sourcePackageVersion).toBe('1.23.0');
754
+
755
+ // A genuine redefinition still lands, and moves the stamp with it.
756
+ const redefined = await bootAsSource('@test/pkg-a', [versioned('changed')], '1.24.0');
757
+ expect(redefined[defaultPolicyTable.name].updates).toBe(1);
758
+ const rewritten = await db.get(defaultPolicyTable, { id: 'v-1' });
759
+ expect(rewritten).toMatchObject({ displayName: 'changed', sourcePackageVersion: '1.24.0' });
760
+ });
761
+
762
+ test('a row with no version stamp is stamped exactly once', async () => {
763
+ const db = getDbAsSystem();
764
+ // Written before versions existed (or by a build that could not resolve its version).
765
+ await bootAsSource('@test/pkg-a', [versioned('same')]);
766
+ expect((await db.get(defaultPolicyTable, { id: 'v-1' })).sourcePackageVersion).toBeFalsy();
767
+
768
+ const first = await bootAsSource('@test/pkg-a', [versioned('same')], '2.0.0');
769
+ expect(first[defaultPolicyTable.name].updates).toBe(1);
770
+ expect((await db.get(defaultPolicyTable, { id: 'v-1' })).sourcePackageVersion).toBe('2.0.0');
771
+
772
+ const second = await bootAsSource('@test/pkg-a', [versioned('same')], '2.0.0');
773
+ expect(second[defaultPolicyTable.name]).toMatchObject({ updates: 0, unchanged: 1 });
774
+ });
775
+
776
+ test('fromDeclaration: derived fields land on insert, backfill existing rows exactly once, and never re-stamp', async () => {
777
+ const db = getDbAsSystem();
778
+ const derivedTable = sourceRecordSyncTestTables.SyncDerivedName;
779
+ // bootBuild names each seeded loader `${source}/SeededLoader${i}` — the declaration's
780
+ // own name is the part after the package, exactly what a migration's class name is.
781
+ const declaration = { table: derivedTable, record: { id: 'd-1', email: 'd1@test.local' } };
782
+
783
+ await boot([declaration]);
784
+ expect(await db.get(derivedTable, { id: 'd-1' })).toMatchObject({ name: 'SeededLoader0' });
785
+
786
+ // A row written before the derived field existed: backfilled ONCE by the next boot…
787
+ await db.update(derivedTable, { id: 'd-1', name: null });
788
+ const backfill = await boot([declaration]);
789
+ expect(backfill[derivedTable.name].updates).toBe(1);
790
+ const backfilled = await db.get(derivedTable, { id: 'd-1' });
791
+ expect(backfilled.name).toBe('SeededLoader0');
792
+
793
+ // …and never again: the derived value is part of the drift comparison like any field.
794
+ const idle = await boot([declaration]);
795
+ expect(idle[derivedTable.name]).toMatchObject({ updates: 0, unchanged: 1 });
796
+ expect((await db.get(derivedTable, { id: 'd-1' })).updated.valueOf()).toBe(backfilled.updated.valueOf());
797
+
798
+ // The derivation owns the field: a value typed into the record does not survive it.
799
+ await boot([{ table: derivedTable, record: { id: 'd-1', email: 'd1@test.local', name: 'typed-by-hand' } }]);
800
+ expect((await db.get(derivedTable, { id: 'd-1' })).name).toBe('SeededLoader0');
801
+ });
802
+ });
803
+
732
804
  test('a natural key without a unique guarantee fails boot loudly', async () => {
733
805
  // Local (unregistered, never created) on purpose: a registered table with an invalid
734
806
  // naturalKey would poison every loader run in the process — validation fires before any
@@ -97,8 +97,33 @@ export class InheritedStampTable extends Table<InheritedStampRecord> {
97
97
  });
98
98
  }
99
99
 
100
+ export interface SyncDerivedNameRecord extends SourceRecord {
101
+ /** Derived by the sync from the DECLARATION (its loader's name) — never typed into the record. */
102
+ name?: string | null;
103
+ email: string;
104
+ }
105
+
106
+ /**
107
+ * The migration-ledger shape: a `name` column the sync fills from the declaration itself
108
+ * (`sourceRecordOptions.fromDeclaration` — the declaring loader's class name), kept on removal
109
+ * like the ledger. The options literal is cast so the suite compiles red-first against a
110
+ * loader that does not yet know the option.
111
+ */
112
+ export class SyncDerivedNameTable extends Table<SyncDerivedNameRecord> {
113
+ name = 'db_test_sync_derived_name';
114
+ columns: Table<SyncDerivedNameRecord>['columns'] = withSourceRecordColumns<SyncDerivedNameRecord>({
115
+ name: new StringColumn('name'),
116
+ email: new StringColumn('email'),
117
+ });
118
+ sourceRecordOptions = {
119
+ onSourceRemoved: 'keep',
120
+ fromDeclaration: (declaration: { name: string }) => ({ name: declaration.name }),
121
+ } as Table<SyncDerivedNameRecord>['sourceRecordOptions'];
122
+ }
123
+
100
124
  export const sourceRecordSyncTestTables = {
101
125
  SyncMachineAccount: new SyncMachineAccountTable() as Table<SyncMachineAccount>,
102
126
  SyncDefaultPolicy: new SyncDefaultPolicyTable() as Table<SyncDefaultPolicyRecord>,
103
127
  InheritedStamp: new InheritedStampTable() as Table<InheritedStampRecord>,
128
+ SyncDerivedName: new SyncDerivedNameTable() as Table<SyncDerivedNameRecord>,
104
129
  };