@dudousxd/nestjs-catalog 0.10.0 → 0.12.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.
@@ -82,10 +82,12 @@ function createCatalogController(path, guards, decorators = []) {
82
82
  registry;
83
83
  service;
84
84
  traces;
85
- constructor(registry, service, traces) {
85
+ workspace;
86
+ constructor(registry, service, traces, workspace) {
86
87
  this.registry = registry;
87
88
  this.service = service;
88
89
  this.traces = traces;
90
+ this.workspace = workspace;
89
91
  }
90
92
  requireTraces() {
91
93
  if (!this.traces) {
@@ -283,6 +285,28 @@ function createCatalogController(path, guards, decorators = []) {
283
285
  response.setHeader('content-disposition', `attachment; filename="${filename}"`);
284
286
  return (0, catalog_query_cache_1.toCsv)(result);
285
287
  }
288
+ /**
289
+ * Every SQL this query has ever been, newest first.
290
+ *
291
+ * `catalog:read`, the same scope `GET saved-queries/:id` asks for, because
292
+ * it hands back the same field one version older. Gating history harder than
293
+ * the current text would be gating the diff rather than the data — the
294
+ * caller can already read what the statement says today, and the authoring
295
+ * scope that `POST`/`PATCH` require is about *choosing what SQL runs*, which
296
+ * reading an old body is not.
297
+ *
298
+ * An empty list is a real answer: a query nobody has edited since revisions
299
+ * shipped may have nothing recorded. A store that keeps none at all says so
300
+ * rather than answering `[]`, because "we do not keep these" and "nothing
301
+ * has changed" are different facts and a screen would draw them the same.
302
+ */
303
+ savedQueryRevisions(id) {
304
+ const workspace = this.workspace;
305
+ if (!workspace || !(0, catalog_workspace_1.supportsSavedQueryRevisions)(workspace)) {
306
+ throw new common_1.BadRequestException("This catalog's workspace store keeps no revisions, so a saved query's SQL can be read but not compared with what it used to be.");
307
+ }
308
+ return workspace.listSavedQueryRevisions(id);
309
+ }
286
310
  dashboards() {
287
311
  return this.service.listDashboards();
288
312
  }
@@ -555,6 +579,14 @@ function createCatalogController(path, guards, decorators = []) {
555
579
  __metadata("design:paramtypes", [String, Object]),
556
580
  __metadata("design:returntype", Promise)
557
581
  ], CatalogController.prototype, "exportSavedQuery", null);
582
+ __decorate([
583
+ (0, common_1.Get)('saved-queries/:id/revisions'),
584
+ (0, catalog_route_auth_1.RequireScopes)('catalog:read'),
585
+ __param(0, (0, common_1.Param)('id')),
586
+ __metadata("design:type", Function),
587
+ __metadata("design:paramtypes", [String]),
588
+ __metadata("design:returntype", void 0)
589
+ ], CatalogController.prototype, "savedQueryRevisions", null);
558
590
  __decorate([
559
591
  (0, common_1.Get)('dashboards'),
560
592
  (0, catalog_route_auth_1.RequireScopes)('catalog:read'),
@@ -667,8 +699,10 @@ function createCatalogController(path, guards, decorators = []) {
667
699
  (0, common_1.Controller)(path),
668
700
  __param(2, (0, common_1.Optional)()),
669
701
  __param(2, (0, common_1.Inject)(catalog_workspace_1.CATALOG_TRACE_STORE)),
702
+ __param(3, (0, common_1.Optional)()),
703
+ __param(3, (0, common_1.Inject)(catalog_workspace_1.CATALOG_WORKSPACE_STORE)),
670
704
  __metadata("design:paramtypes", [catalog_registry_base_1.CatalogRegistry,
671
- catalog_service_1.CatalogService, Object])
705
+ catalog_service_1.CatalogService, Object, Object])
672
706
  ], CatalogController);
673
707
  if (guards.length > 0) {
674
708
  (0, common_1.UseGuards)(...guards)(CatalogController);
@@ -7,6 +7,7 @@
7
7
  * schedules, retries and checkpoints, and writing a second one would mean two
8
8
  * systems each believing they decide when a load runs.
9
9
  */
10
+ import type { CatalogRevision } from './catalog.workspace';
10
11
  /**
11
12
  * Where a connector pulls from.
12
13
  *
@@ -136,6 +137,14 @@ export declare function isTransformLanguage(value: unknown): value is TransformL
136
137
  * Versioned, because a load that produced surprising numbers is investigated
137
138
  * afterwards, and "which code ran" is the first question. Bumping the version
138
139
  * on every change costs a row and answers it.
140
+ *
141
+ * The version used to be the *whole* answer, and it was half of one: it named
142
+ * code that no longer existed anywhere, because one row per transform is
143
+ * overwritten in place. Each version's code is now recorded as a
144
+ * {@link CatalogRevision}, read through
145
+ * {@link CatalogPipelineStore.listTransformRevisions}, so the number on a run
146
+ * and the text it names are both retrievable. `version` still counts saves that
147
+ * changed the code and nothing else — see `saveTransform`.
139
148
  */
140
149
  export interface CatalogTransform {
141
150
  id: string;
@@ -223,17 +232,26 @@ export interface ConnectorRun {
223
232
  error?: string;
224
233
  startedAt: string;
225
234
  finishedAt?: string;
226
- /** Which transform version ran, so a surprising load can be traced to code. */
235
+ /**
236
+ * Which transform version ran, so a surprising load can be traced to code.
237
+ *
238
+ * Traced to the code itself, now, and not only to a number: the version this
239
+ * names is the version of a {@link CatalogRevision}, so `transforms/:id/revisions`
240
+ * answers with the body that produced these rows. Until that route existed
241
+ * this field could only establish *that* the transform had been edited since.
242
+ */
227
243
  transformVersion?: number;
228
244
  /** Which workflow ran, when the connector delegated to one. */
229
245
  workflowId?: string;
230
246
  /**
231
247
  * Which *version* of it ran.
232
248
  *
233
- * The same question `transformVersion` answers, asked of the graph. A
234
- * workflow keeps only its latest shape exactly as a transform keeps only
235
- * its latest code so this number is what connects a run to the graph that
236
- * produced it, and the only way to know a graph has changed since.
249
+ * The same question `transformVersion` answers, asked of the graph — and no
250
+ * longer answered as well, which is worth knowing before relying on it. A
251
+ * workflow keeps only its latest shape (see {@link CatalogWorkflow} for why it
252
+ * is excluded from revisions while a transform is not), so this number
253
+ * connects a run to the graph that produced it and is the only way to know a
254
+ * graph has changed since. It cannot produce the graph.
237
255
  */
238
256
  workflowVersion?: number;
239
257
  /**
@@ -435,20 +453,77 @@ export interface WorkflowGraph {
435
453
  * the code; for a workflow it means the code *and* the wiring, so both the
436
454
  * graph version and the per-node transform versions are recorded on the run.
437
455
  *
438
- * Like a transform, only the latest shape is kept. Storing every past graph was
439
- * the alternative and was rejected for consistency: transforms already answer
440
- * "which code ran" with a number and no history, and a model where the graph is
441
- * fully recoverable but the code inside it is not would give false confidence in
442
- * an audit. The limitation is real and worth stating plainly — an edited graph
443
- * cannot be reconstructed from an old run, only identified as different.
456
+ * **Only the latest shape is kept, and unlike a transform it is not
457
+ * revisioned.** That asymmetry is a decision rather than an oversight, and this
458
+ * is where somebody looking for the missing feature will look, so it is argued
459
+ * here.
460
+ *
461
+ * A transform's code and a saved query's SQL are text a person typed, and
462
+ * {@link CatalogRevision} archives text: two bodies, a line differ, done. A
463
+ * graph is a structure. Its "body" would be JSON nobody wrote, and a text diff
464
+ * over it is dominated by key order and canvas positions — it would report a
465
+ * dragged box as a change to what the load does, which is the opposite of what
466
+ * {@link workflowGraphHash} is careful to exclude. Diffing graphs is a graph
467
+ * problem and deserves a screen that draws one, not a line differ pointed at
468
+ * serialised nodes.
469
+ *
470
+ * The decisive reason is the counter. {@link version} is bumped on **draft**
471
+ * edits deliberately — see the note on it — so that a run's `workflowVersion`
472
+ * can never mean two different graphs. Archiving one body per version would
473
+ * therefore store every autosave of a canvas somebody is still dragging boxes
474
+ * around on, and under the per-subject cap that {@link CATALOG_REVISION_LIMIT}
475
+ * imposes, that noise would evict the versions that actually ran. A counter
476
+ * designed to be cheap to inflate and an archive designed to be bounded do not
477
+ * compose; making them compose means keying the archive on behaviour rather than
478
+ * on saves, which is what `graphHash` already is, and that is a different
479
+ * feature from this one.
480
+ *
481
+ * So the limitation stays, stated plainly: an edited graph cannot be
482
+ * reconstructed from an old run, only identified as different. A diff screen
483
+ * answers for the code and the SQL and not for the wiring.
444
484
  */
485
+ /**
486
+ * Whether this graph is still being drawn, or is something somebody declared
487
+ * finished.
488
+ *
489
+ * The distinction exists because validation used to be the gate on *saving*, and
490
+ * that made an unfinished graph unstorable: `saveWorkflow` refused anything
491
+ * `validateWorkflow` had an issue with, so a canvas with one node on it could
492
+ * not be written down at all and closing the tab lost it. Worse, it made the
493
+ * canvas lie about ordinary work — clicking "+ Sink" produces a node that is
494
+ * unreachable from any source and names no type, both true and both useless one
495
+ * second after the click, because a just-added node is unwired by construction.
496
+ *
497
+ * So the gate moved rather than loosened. Validation is now the gate on
498
+ * publishing, and the same `validateWorkflow` still decides — a draft is not a
499
+ * graph that skipped the rules, it is a graph nobody has claimed is finished
500
+ * yet. Everything that consumes a workflow asks for `ready`: a connector may
501
+ * only point at one, and a promotion may only carry one. What crosses an
502
+ * environment should be something a person declared done.
503
+ */
504
+ export declare const WORKFLOW_STATUSES: readonly ["draft", "ready"];
505
+ export type WorkflowStatus = (typeof WORKFLOW_STATUSES)[number];
506
+ /** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
507
+ export declare function isWorkflowStatus(value: unknown): value is WorkflowStatus;
445
508
  export interface CatalogWorkflow {
446
509
  id: string;
447
510
  name: string;
448
511
  description?: string;
449
512
  nodes: WorkflowNode[];
450
513
  edges: WorkflowEdge[];
451
- /** Bumped whenever the graph's behaviour changes. Never on a rename or a move. */
514
+ /** See {@link WORKFLOW_STATUSES}. A graph is `draft` until somebody publishes it. */
515
+ status: WorkflowStatus;
516
+ /**
517
+ * Bumped whenever the graph's behaviour changes. Never on a rename or a move.
518
+ *
519
+ * Bumped on a **draft** edit too, which looks like exactly the inflation this
520
+ * rule exists to prevent and is not. The counter's job is to make
521
+ * {@link ConnectorRun.workflowVersion} answer "which shape ran": freezing it
522
+ * while a graph is drafted would let a run recorded at v4 and a later run also
523
+ * at v4 mean two different graphs, which is the one thing that field must
524
+ * never do. Drafting therefore inflates a number nobody reads — cheap — rather
525
+ * than making a number somebody does read ambiguous.
526
+ */
452
527
  version: number;
453
528
  /** Fingerprint of the graph at this version. See {@link workflowGraphHash}. */
454
529
  graphHash: string;
@@ -660,7 +735,24 @@ export interface CatalogWorkflowStore {
660
735
  listWorkflows(): Promise<CatalogWorkflow[]>;
661
736
  getWorkflow(id: string): Promise<CatalogWorkflow | undefined>;
662
737
  /**
663
- * Validates before it writes, and refuses naming the node.
738
+ * Writes. Validates only what it must.
739
+ *
740
+ * A **draft** is written without validating, which is the whole of the change
741
+ * and the reason {@link WORKFLOW_STATUSES} exists: a graph you have not
742
+ * finished has to be storable, or closing the tab loses it. A **ready**
743
+ * workflow is still validated on every save, because it is the one that runs.
744
+ *
745
+ * `status` is not an input. A save cannot promote a draft to ready — that is
746
+ * {@link publishWorkflow}, which exists so there is one place that validates
747
+ * and one place that can explain why it refused. A save of an already-ready
748
+ * workflow keeps it ready, and **refuses an edit that would make it invalid**
749
+ * rather than quietly demoting it to draft. Demotion was the other option and
750
+ * it is the one that loses a running pipeline silently: a connector may only
751
+ * point at a ready graph, so a save that dropped the status would disable a
752
+ * scheduled load with nothing said to anybody. Refusing puts the error in
753
+ * front of the person who is editing, at the moment they edit. To park a
754
+ * broken idea on a live graph, {@link unpublishWorkflow} it first and be told
755
+ * which connectors that stops.
664
756
  *
665
757
  * `version`, `graphHash` and `targetType` are not inputs: the first two are
666
758
  * derived from the graph and the third from the sink, and accepting them from
@@ -670,6 +762,34 @@ export interface CatalogWorkflowStore {
670
762
  id?: string;
671
763
  description?: string;
672
764
  }, createdBy: string): Promise<CatalogWorkflow>;
765
+ /**
766
+ * Declare a graph finished: validate it, and make it `ready`.
767
+ *
768
+ * A transition rather than a field on save, and the argument is that this is
769
+ * the only shape with somewhere to put the refusal. "Ready" is a claim that
770
+ * has to be checked, and a check that fails owes an explanation naming the
771
+ * nodes — `validateWorkflow` produces exactly that, and a boolean field on a
772
+ * save request has nowhere to return it that is not an error on an operation
773
+ * the caller thought was about something else. It also makes the audit
774
+ * question answerable: publishing is an act with an actor, and a field set in
775
+ * passing during an autosave is not.
776
+ *
777
+ * Idempotent on an already-ready graph, because the honest answer to "publish
778
+ * this thing that is published" is the graph, not an error.
779
+ */
780
+ publishWorkflow(id: string, publishedBy: string): Promise<CatalogWorkflow>;
781
+ /**
782
+ * Take a graph back to `draft`.
783
+ *
784
+ * **Refuses while any connector still runs it**, exactly as
785
+ * {@link deleteWorkflow} does and for the same reason: a connector may only
786
+ * point at a ready graph, so unpublishing one out from under a schedule breaks
787
+ * a load that was working, and the operator needs to know *which* connectors
788
+ * to point elsewhere first. Refusing here rather than cascading is deliberate —
789
+ * disabling somebody's connectors as a side effect of an edit to something
790
+ * else is precisely the silent action this status exists to prevent.
791
+ */
792
+ unpublishWorkflow(id: string, unpublishedBy: string): Promise<CatalogWorkflow>;
673
793
  /** Refuses while any connector still runs it. */
674
794
  deleteWorkflow(id: string): Promise<boolean>;
675
795
  /** Which connectors run it. Named, so a refusal can say. */
@@ -724,6 +844,13 @@ export interface CatalogStageStore {
724
844
  * does, because a flag is a claim and a method is the thing itself.
725
845
  */
726
846
  export declare function supportsWorkflows(store: CatalogPipelineStore): store is CatalogPipelineStore & CatalogWorkflowStore;
847
+ /**
848
+ * Whether this store keeps a transform's history.
849
+ *
850
+ * The method rather than a flag, exactly as {@link supportsWorkflows} argues: a
851
+ * flag is a claim and a method is the thing itself.
852
+ */
853
+ export declare function supportsTransformRevisions(store: CatalogPipelineStore): store is CatalogPipelineStore & Required<Pick<CatalogPipelineStore, 'listTransformRevisions'>>;
727
854
  export declare function supportsWorkflowStages(store: CatalogPipelineStore): store is CatalogPipelineStore & CatalogStageStore;
728
855
  export interface CatalogPipelineStore extends Partial<CatalogWorkflowStore>, Partial<CatalogStageStore> {
729
856
  listConnectors(): Promise<CatalogConnector[]>;
@@ -762,6 +889,22 @@ export interface CatalogPipelineStore extends Partial<CatalogWorkflowStore>, Par
762
889
  description?: string;
763
890
  }, createdBy: string): Promise<CatalogTransform>;
764
891
  deleteTransform(id: string): Promise<boolean>;
892
+ /**
893
+ * Every version of this transform's code, newest first.
894
+ *
895
+ * **Optional**, mixed in for the same reason {@link CatalogWorkflowStore} is:
896
+ * a store written against the previous shape of this interface still
897
+ * implements it, and turning that into a compile error would be a breaking
898
+ * change for an additive feature. {@link supportsTransformRevisions} is how a
899
+ * caller asks, so a deployment whose store keeps no history gets a sentence
900
+ * rather than a method that is missing at run time.
901
+ *
902
+ * The list is what makes `transformVersion` on a run mean something: the
903
+ * revision whose {@link CatalogRevision.version} equals it holds the code that
904
+ * produced those rows. Bounded — see {@link CATALOG_REVISION_LIMIT} for what
905
+ * that bound costs.
906
+ */
907
+ listTransformRevisions?(id: string): Promise<CatalogRevision[]>;
765
908
  startRun(input: {
766
909
  connectorId: string;
767
910
  snapshotId: string;
@@ -9,10 +9,11 @@
9
9
  * systems each believing they decide when a load runs.
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.CATALOG_PIPELINE_STORE = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_EXECUTION_MODES = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_NODE_KINDS = exports.TRANSFORM_RUNNER = exports.TRANSFORM_LANGUAGES = exports.CONNECTOR_KINDS = void 0;
12
+ exports.CATALOG_PIPELINE_STORE = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_EXECUTION_MODES = exports.WORKFLOW_STATUSES = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_NODE_KINDS = exports.TRANSFORM_RUNNER = exports.TRANSFORM_LANGUAGES = exports.CONNECTOR_KINDS = void 0;
13
13
  exports.isConnectorKind = isConnectorKind;
14
14
  exports.isTransformLanguage = isTransformLanguage;
15
15
  exports.isWorkflowNodeKind = isWorkflowNodeKind;
16
+ exports.isWorkflowStatus = isWorkflowStatus;
16
17
  exports.isWorkflowExecutionMode = isWorkflowExecutionMode;
17
18
  exports.validateWorkflow = validateWorkflow;
18
19
  exports.workflowRunOrder = workflowRunOrder;
@@ -20,6 +21,7 @@ exports.workflowGraphHash = workflowGraphHash;
20
21
  exports.isWorkflowNode = isWorkflowNode;
21
22
  exports.isWorkflowEdge = isWorkflowEdge;
22
23
  exports.supportsWorkflows = supportsWorkflows;
24
+ exports.supportsTransformRevisions = supportsTransformRevisions;
23
25
  exports.supportsWorkflowStages = supportsWorkflowStages;
24
26
  exports.isPipelineStore = isPipelineStore;
25
27
  /**
@@ -136,6 +138,79 @@ function isWorkflowNodeKind(value) {
136
138
  * separator would let one node read another's rows.
137
139
  */
138
140
  exports.WORKFLOW_NODE_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
141
+ /**
142
+ * An authored graph of steps ending in one commit.
143
+ *
144
+ * Versioned the way {@link CatalogTransform} is, and for the same question: a
145
+ * load that produced surprising numbers is investigated afterwards, and "what
146
+ * ran" is the first thing asked. For a single-transform connector that means
147
+ * the code; for a workflow it means the code *and* the wiring, so both the
148
+ * graph version and the per-node transform versions are recorded on the run.
149
+ *
150
+ * **Only the latest shape is kept, and unlike a transform it is not
151
+ * revisioned.** That asymmetry is a decision rather than an oversight, and this
152
+ * is where somebody looking for the missing feature will look, so it is argued
153
+ * here.
154
+ *
155
+ * A transform's code and a saved query's SQL are text a person typed, and
156
+ * {@link CatalogRevision} archives text: two bodies, a line differ, done. A
157
+ * graph is a structure. Its "body" would be JSON nobody wrote, and a text diff
158
+ * over it is dominated by key order and canvas positions — it would report a
159
+ * dragged box as a change to what the load does, which is the opposite of what
160
+ * {@link workflowGraphHash} is careful to exclude. Diffing graphs is a graph
161
+ * problem and deserves a screen that draws one, not a line differ pointed at
162
+ * serialised nodes.
163
+ *
164
+ * The decisive reason is the counter. {@link version} is bumped on **draft**
165
+ * edits deliberately — see the note on it — so that a run's `workflowVersion`
166
+ * can never mean two different graphs. Archiving one body per version would
167
+ * therefore store every autosave of a canvas somebody is still dragging boxes
168
+ * around on, and under the per-subject cap that {@link CATALOG_REVISION_LIMIT}
169
+ * imposes, that noise would evict the versions that actually ran. A counter
170
+ * designed to be cheap to inflate and an archive designed to be bounded do not
171
+ * compose; making them compose means keying the archive on behaviour rather than
172
+ * on saves, which is what `graphHash` already is, and that is a different
173
+ * feature from this one.
174
+ *
175
+ * So the limitation stays, stated plainly: an edited graph cannot be
176
+ * reconstructed from an old run, only identified as different. A diff screen
177
+ * answers for the code and the SQL and not for the wiring.
178
+ */
179
+ /**
180
+ * Whether this graph is still being drawn, or is something somebody declared
181
+ * finished.
182
+ *
183
+ * The distinction exists because validation used to be the gate on *saving*, and
184
+ * that made an unfinished graph unstorable: `saveWorkflow` refused anything
185
+ * `validateWorkflow` had an issue with, so a canvas with one node on it could
186
+ * not be written down at all and closing the tab lost it. Worse, it made the
187
+ * canvas lie about ordinary work — clicking "+ Sink" produces a node that is
188
+ * unreachable from any source and names no type, both true and both useless one
189
+ * second after the click, because a just-added node is unwired by construction.
190
+ *
191
+ * So the gate moved rather than loosened. Validation is now the gate on
192
+ * publishing, and the same `validateWorkflow` still decides — a draft is not a
193
+ * graph that skipped the rules, it is a graph nobody has claimed is finished
194
+ * yet. Everything that consumes a workflow asks for `ready`: a connector may
195
+ * only point at one, and a promotion may only carry one. What crosses an
196
+ * environment should be something a person declared done.
197
+ */
198
+ exports.WORKFLOW_STATUSES = [
199
+ /**
200
+ * Being drawn. Saves without validating, and cannot run, be scheduled, or be
201
+ * promoted. An incomplete node here is the normal state rather than an alarm.
202
+ */
203
+ 'draft',
204
+ /**
205
+ * Declared finished, and validated at the moment it was declared. This is the
206
+ * only status a connector may point at and the only one a promotion carries.
207
+ */
208
+ 'ready',
209
+ ];
210
+ /** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
211
+ function isWorkflowStatus(value) {
212
+ return exports.WORKFLOW_STATUSES.some((status) => status === value);
213
+ }
139
214
  /**
140
215
  * How a workflow run is executed here.
141
216
  *
@@ -634,7 +709,22 @@ function isWorkflowEdge(value) {
634
709
  function supportsWorkflows(store) {
635
710
  return (typeof store.listWorkflows === 'function' &&
636
711
  typeof store.getWorkflow === 'function' &&
637
- typeof store.saveWorkflow === 'function');
712
+ typeof store.saveWorkflow === 'function' &&
713
+ // Asked for by name like the rest, rather than assumed to come with
714
+ // `saveWorkflow`. Promotion publishes what it saves, so a store that has the
715
+ // save and not the transition would narrow cleanly here and then fail one
716
+ // call later, in the middle of an apply that has already written types and
717
+ // transforms into the target.
718
+ typeof store.publishWorkflow === 'function');
719
+ }
720
+ /**
721
+ * Whether this store keeps a transform's history.
722
+ *
723
+ * The method rather than a flag, exactly as {@link supportsWorkflows} argues: a
724
+ * flag is a claim and a method is the thing itself.
725
+ */
726
+ function supportsTransformRevisions(store) {
727
+ return typeof store.listTransformRevisions === 'function';
638
728
  }
639
729
  function supportsWorkflowStages(store) {
640
730
  return typeof store.writeStage === 'function' && typeof store.readStage === 'function';