@b2bc-devkit/sheetorm 1.0.3 → 1.2.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.
@@ -14,10 +14,10 @@
14
14
  * every Jest `test()` block. Each handler exercises the same logic but using
15
15
  * real Google Sheets API calls instead of mocks.
16
16
  * - {@link RuntimeParity} — public API (static methods) invoked from GAS menu:
17
- * - `runStageOne()` / `runStageTwo()` / `runStageThree()` — execute test subsets.
17
+ * - `runStageOne()` / `runStageTwo()` / `runStageThree()` / `runStageFour()` — execute test subsets.
18
18
  * - `validate()` — cross-check collected results against {@link ParityCatalog}.
19
19
  *
20
- * Tests are split into three stages to stay within the 6-minute GAS execution
20
+ * Tests are split into four stages to stay within the 6-minute GAS execution
21
21
  * time limit. Each stage persists results to a `__parity_results_*` sheet so
22
22
  * that `validate()` can aggregate them later.
23
23
  *
@@ -2975,6 +2975,161 @@ const runtimeSuiteHandlers = {
2975
2975
  }
2976
2976
  assertTrue(threw, "save() should throw when indexed schema has no indexTableName");
2977
2977
  },
2978
+ "protects sheet on first save when isProtected returns true": (ctx) => {
2979
+ const adapter = ctx.state.getAdapter();
2980
+ const suffix = ctx.state.nextTableName("prot");
2981
+ Registry.reset();
2982
+ resetDecoratorCaches();
2983
+ Registry.getInstance().configure({ adapter });
2984
+ class ProtectedNote extends BaseRecord {
2985
+ static get tableName() {
2986
+ return `ProtNote_${suffix}`;
2987
+ }
2988
+ static isProtected() {
2989
+ return true;
2990
+ }
2991
+ static protectedFor() {
2992
+ return ["alice@example.com", "bob@example.com"];
2993
+ }
2994
+ }
2995
+ const note = new ProtectedNote();
2996
+ note.body = "secret";
2997
+ note.save();
2998
+ // Verify the sheet was created and protected
2999
+ const spreadsheet = ctx.state.getSpreadsheet();
3000
+ const sheet = spreadsheet.getSheetByName(`ProtNote_${suffix}`);
3001
+ assertTrue(sheet !== null, "protected sheet should exist");
3002
+ const protections = sheet.getProtections(SpreadsheetApp.ProtectionType.SHEET);
3003
+ assertTrue(protections.length > 0, "sheet should have at least one protection");
3004
+ const editors = protections[0].getEditors().map((e) => e.getEmail());
3005
+ assertTrue(editors.includes("alice@example.com"), "alice should be an editor");
3006
+ assertTrue(editors.includes("bob@example.com"), "bob should be an editor");
3007
+ },
3008
+ "does not protect sheet when isProtected returns false": (ctx) => {
3009
+ const { Car } = setup(ctx);
3010
+ const car = new Car();
3011
+ car.make = "Toyota";
3012
+ car.model = "Corolla";
3013
+ car.save();
3014
+ const spreadsheet = ctx.state.getSpreadsheet();
3015
+ const sheet = spreadsheet.getSheetByName(Car.tableName);
3016
+ assertTrue(sheet !== null, "sheet should exist");
3017
+ const protections = sheet.getProtections(SpreadsheetApp.ProtectionType.SHEET);
3018
+ assertEqual(protections.length, 0, "unprotected sheet should have no protections");
3019
+ },
3020
+ "does not re-protect sheet on subsequent saves": (ctx) => {
3021
+ const adapter = ctx.state.getAdapter();
3022
+ const suffix = ctx.state.nextTableName("reprot");
3023
+ Registry.reset();
3024
+ resetDecoratorCaches();
3025
+ Registry.getInstance().configure({ adapter });
3026
+ class TrackedProtected extends BaseRecord {
3027
+ static get tableName() {
3028
+ return `Tracked_${suffix}`;
3029
+ }
3030
+ static isProtected() {
3031
+ return true;
3032
+ }
3033
+ static protectedFor() {
3034
+ return ["admin@example.com"];
3035
+ }
3036
+ }
3037
+ const item1 = new TrackedProtected();
3038
+ item1.value = "first";
3039
+ item1.save();
3040
+ const spreadsheet = ctx.state.getSpreadsheet();
3041
+ const sheet = spreadsheet.getSheetByName(`Tracked_${suffix}`);
3042
+ assertTrue(sheet !== null, "sheet should exist");
3043
+ const protsBefore = sheet.getProtections(SpreadsheetApp.ProtectionType.SHEET);
3044
+ assertEqual(protsBefore.length, 1, "should have exactly one protection after first save");
3045
+ const item2 = new TrackedProtected();
3046
+ item2.value = "second";
3047
+ item2.save();
3048
+ const protsAfter = sheet.getProtections(SpreadsheetApp.ProtectionType.SHEET);
3049
+ assertEqual(protsAfter.length, 1, "should still have exactly one protection after second save");
3050
+ },
3051
+ "protects sheet with empty editors list": (ctx) => {
3052
+ const adapter = ctx.state.getAdapter();
3053
+ const suffix = ctx.state.nextTableName("locked");
3054
+ Registry.reset();
3055
+ resetDecoratorCaches();
3056
+ Registry.getInstance().configure({ adapter });
3057
+ class LockedSheet extends BaseRecord {
3058
+ static get tableName() {
3059
+ return `Locked_${suffix}`;
3060
+ }
3061
+ static isProtected() {
3062
+ return true;
3063
+ }
3064
+ }
3065
+ const item = new LockedSheet();
3066
+ item.data = "locked";
3067
+ item.save();
3068
+ const spreadsheet = ctx.state.getSpreadsheet();
3069
+ const sheet = spreadsheet.getSheetByName(`Locked_${suffix}`);
3070
+ assertTrue(sheet !== null, "sheet should exist");
3071
+ const protections = sheet.getProtections(SpreadsheetApp.ProtectionType.SHEET);
3072
+ assertTrue(protections.length > 0, "sheet should be protected even with no editors");
3073
+ },
3074
+ "hides sheet on first save when isHidden returns true": (ctx) => {
3075
+ const adapter = ctx.state.getAdapter();
3076
+ const suffix = ctx.state.nextTableName("hidden");
3077
+ Registry.reset();
3078
+ resetDecoratorCaches();
3079
+ Registry.getInstance().configure({ adapter });
3080
+ class HiddenLog extends BaseRecord {
3081
+ static get tableName() {
3082
+ return `HidLog_${suffix}`;
3083
+ }
3084
+ static isHidden() {
3085
+ return true;
3086
+ }
3087
+ }
3088
+ const log = new HiddenLog();
3089
+ log.entry = "secret log";
3090
+ log.save();
3091
+ const spreadsheet = ctx.state.getSpreadsheet();
3092
+ const sheet = spreadsheet.getSheetByName(`HidLog_${suffix}`);
3093
+ assertTrue(sheet !== null, "hidden sheet should exist");
3094
+ assertTrue(sheet.isSheetHidden(), "sheet should be hidden");
3095
+ },
3096
+ "does not hide sheet when isHidden returns false": (ctx) => {
3097
+ const { Car } = setup(ctx);
3098
+ const car = new Car();
3099
+ car.make = "Toyota";
3100
+ car.model = "Corolla";
3101
+ car.save();
3102
+ const spreadsheet = ctx.state.getSpreadsheet();
3103
+ const sheet = spreadsheet.getSheetByName(Car.tableName);
3104
+ assertTrue(sheet !== null, "sheet should exist");
3105
+ assertTrue(!sheet.isSheetHidden(), "sheet should not be hidden");
3106
+ },
3107
+ "does not re-hide sheet on subsequent saves": (ctx) => {
3108
+ const adapter = ctx.state.getAdapter();
3109
+ const suffix = ctx.state.nextTableName("rehid");
3110
+ Registry.reset();
3111
+ resetDecoratorCaches();
3112
+ Registry.getInstance().configure({ adapter });
3113
+ class TrackedHidden extends BaseRecord {
3114
+ static get tableName() {
3115
+ return `TrkHid_${suffix}`;
3116
+ }
3117
+ static isHidden() {
3118
+ return true;
3119
+ }
3120
+ }
3121
+ const item1 = new TrackedHidden();
3122
+ item1.value = "first";
3123
+ item1.save();
3124
+ const spreadsheet = ctx.state.getSpreadsheet();
3125
+ const sheet = spreadsheet.getSheetByName(`TrkHid_${suffix}`);
3126
+ assertTrue(sheet !== null, "sheet should exist");
3127
+ assertTrue(sheet.isSheetHidden(), "sheet should be hidden after first save");
3128
+ const item2 = new TrackedHidden();
3129
+ item2.value = "second";
3130
+ item2.save();
3131
+ assertTrue(sheet.isSheetHidden(), "sheet should still be hidden after second save");
3132
+ },
2978
3133
  };
2979
3134
  })(),
2980
3135
  "sheet-repository.test.ts": {
@@ -3996,14 +4151,20 @@ function runTestsStageOne() {
3996
4151
  validateTests();
3997
4152
  return runTestsForSuites(ParityCatalog.SUITES.slice(0, 4));
3998
4153
  }
3999
- /** Stage 2: serialization, uuid, record (~126 tests, ~250s). */
4154
+ /** Stage 2: serialization, uuid (~52 tests, ~60s). */
4000
4155
  function runTestsStageTwo() {
4001
4156
  SheetOrmLogger.verbose = false;
4002
4157
  validateTests();
4003
- return runTestsForSuites(ParityCatalog.SUITES.slice(4, 7));
4158
+ return runTestsForSuites(ParityCatalog.SUITES.slice(4, 6));
4004
4159
  }
4005
- /** Stage 3: sheet-repository (~35 tests, ~120s). */
4160
+ /** Stage 3: record (~81 tests, ~250s). */
4006
4161
  function runTestsStageThree() {
4162
+ SheetOrmLogger.verbose = false;
4163
+ validateTests();
4164
+ return runTestsForSuites(ParityCatalog.SUITES.slice(6, 7));
4165
+ }
4166
+ /** Stage 4: sheet-repository (~35 tests, ~120s). */
4167
+ function runTestsStageFour() {
4007
4168
  SheetOrmLogger.verbose = false;
4008
4169
  validateTests();
4009
4170
  return runTestsForSuites(ParityCatalog.SUITES.slice(7));
@@ -4019,10 +4180,12 @@ export class RuntimeParity {
4019
4180
  }
4020
4181
  /** Execute stage-one parity tests (cache, index-store, query, query-engine). */
4021
4182
  RuntimeParity.runStageOne = runTestsStageOne;
4022
- /** Execute stage-two parity tests (serialization, uuid, record). */
4183
+ /** Execute stage-two parity tests (serialization, uuid). */
4023
4184
  RuntimeParity.runStageTwo = runTestsStageTwo;
4024
- /** Execute stage-three parity tests (sheet-repository). */
4185
+ /** Execute stage-three parity tests (record). */
4025
4186
  RuntimeParity.runStageThree = runTestsStageThree;
4187
+ /** Execute stage-four parity tests (sheet-repository). */
4188
+ RuntimeParity.runStageFour = runTestsStageFour;
4026
4189
  /** Cross-check that all ParityCatalog case IDs have runtime handlers. */
4027
4190
  RuntimeParity.validate = validateTests;
4028
4191
  /** Complete list of runtime parity case IDs (for external cross-reference). */