@theseam/ui-common 1.0.2-beta.51 → 1.0.2-beta.57

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.
@@ -1,14 +1,16 @@
1
1
  import { ApolloLink, NetworkStatus, Observable as Observable$1 } from '@apollo/client/core';
2
2
  import { print, visit as visit$1, parse, BREAK as BREAK$1, parseValue, Kind, valueFromASTUntyped } from 'graphql';
3
- import { hasProperty, notNullOrUndefined, isNullOrUndefined, withoutProperty, wrapIntoObservable, withoutProperties } from '@theseam/ui-common/utils';
4
3
  import { visit, BREAK } from 'graphql/language';
5
4
  import * as i0 from '@angular/core';
6
5
  import { isDevMode, InjectionToken, Optional, Inject, Injectable, EventEmitter } from '@angular/core';
6
+ import { hasProperty, notNullOrUndefined, isNullOrUndefined, withoutProperty, wrapIntoObservable, withoutProperties } from '@theseam/ui-common/utils';
7
7
  import { Observable, EMPTY, from, defer, of, combineLatest, Subscription, Subject, BehaviorSubject, isObservable } from 'rxjs';
8
8
  import { switchMap, tap, take, concatMap, filter, toArray, map, auditTime, distinctUntilChanged, finalize, catchError, shareReplay, startWith, skip } from 'rxjs/operators';
9
9
  import * as i1 from 'apollo-angular';
10
10
  import { gql, provideApollo } from 'apollo-angular';
11
11
  import { THESEAM_COLUMNS_DATA_FILTER_DATE_TEXT_SEARCH_TYPES, getFormattedDateForComparison, THESEAM_COLUMNS_DATA_FILTER_DATE_RANGE_SEARCH_TYPES, THESEAM_COLUMNS_DATA_FILTER_NUMERIC_TEXT_SEARCH_TYPES, THESEAM_COLUMNS_DATA_FILTER_NUMERIC_RANGE_SEARCH_TYPES, THESEAM_COLUMNS_DATA_FILTER_TEXT_TEXT_SEARCH_TYPES } from '@theseam/ui-common/datatable';
12
+ import { faFileCsv } from '@fortawesome/free-solid-svg-icons';
13
+ import FileSaver from 'file-saver';
12
14
  import { concat, InMemoryCache } from '@apollo/client';
13
15
 
14
16
  /**
@@ -44,35 +46,15 @@ function logQueryLink(inner, options) {
44
46
  return ApolloLink.from([beforeLink, inner, afterLink]);
45
47
  }
46
48
 
47
- class GQLDirection {
48
- direction;
49
- constructor(direction) {
50
- this.direction = direction;
51
- }
52
- static ASC = new GQLDirection('ASC');
53
- static DESC = new GQLDirection('DESC');
54
- }
55
-
56
- var HintsKind;
57
- (function (HintsKind) {
58
- HintsKind["OperationDefinition"] = "OperationDefinition";
59
- HintsKind["Field"] = "Field";
60
- HintsKind["VariableDefinition"] = "VariableDefinition";
61
- HintsKind["Variable"] = "Variable";
62
- HintsKind["Argument"] = "Argument";
63
- })(HintsKind || (HintsKind = {}));
64
-
65
- const DEFAULT_TO_REMOVE_ON_UNDEFINED = ['where', 'order'];
66
-
67
- class GQLVariable {
68
- name;
69
- type;
70
- constructor(name, type) {
71
- this.name = name;
72
- this.type = type;
73
- }
74
- }
75
-
49
+ /**
50
+ * Checks whether a GraphQL document or value node contains a reference to
51
+ * the named variable.
52
+ *
53
+ * This includes references inside variable definitions — a variable that is
54
+ * defined but not used as an argument will still be found. This behavior is
55
+ * relied on by `removeIfNotUsed` processing to avoid removing variables that
56
+ * may be needed after a later inline step.
57
+ */
76
58
  function containsVariable(node, variableName) {
77
59
  let found = false;
78
60
  visit(node, {
@@ -86,10 +68,32 @@ function containsVariable(node, variableName) {
86
68
  return found;
87
69
  }
88
70
 
71
+ /**
72
+ * Creates a variable reference marker for use in filter objects that will be
73
+ * passed to `toGQL` and inlined into a query.
74
+ *
75
+ * When `toGQL` encounters an object with a `gqlVar` property, it emits the
76
+ * value as-is (e.g. `$search`), producing a GraphQL variable reference in the
77
+ * output rather than a string literal.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * const filter = { name: { contains: gqlVar('search') } }
82
+ * // toGQL(filter) → '{name: {contains: $search}}'
83
+ * ```
84
+ */
89
85
  function gqlVar(varName) {
90
86
  return { gqlVar: `$${varName}` };
91
87
  }
92
88
 
89
+ /**
90
+ * Replaces all references to a variable in the query body with a literal
91
+ * value. If the value represents `undefined`, the variable references are
92
+ * removed from the query instead.
93
+ *
94
+ * This does not remove the variable definition — use `removeVariableDefinition`
95
+ * separately if needed.
96
+ */
93
97
  function inlineVariable(query, variableName, variableValue) {
94
98
  // return visit(query, {
95
99
  // VariableDefinition(node) {
@@ -163,6 +167,35 @@ function parseComments(ast) {
163
167
  return comments;
164
168
  }
165
169
 
170
+ class GQLDirection {
171
+ direction;
172
+ constructor(direction) {
173
+ this.direction = direction;
174
+ }
175
+ static ASC = new GQLDirection('ASC');
176
+ static DESC = new GQLDirection('DESC');
177
+ }
178
+
179
+ var HintsKind;
180
+ (function (HintsKind) {
181
+ HintsKind["OperationDefinition"] = "OperationDefinition";
182
+ HintsKind["Field"] = "Field";
183
+ HintsKind["VariableDefinition"] = "VariableDefinition";
184
+ HintsKind["Variable"] = "Variable";
185
+ HintsKind["Argument"] = "Argument";
186
+ })(HintsKind || (HintsKind = {}));
187
+
188
+ const DEFAULT_TO_REMOVE_ON_UNDEFINED = ['where', 'order'];
189
+
190
+ class GQLVariable {
191
+ name;
192
+ type;
193
+ constructor(name, type) {
194
+ this.name = name;
195
+ this.type = type;
196
+ }
197
+ }
198
+
166
199
  const HINT_PREFIX_REGEX = /^\s*@gql-hint:.+/;
167
200
  const HINT_NAMES_CAPTURE_REGEX = /^\s*@gql-hint:([a-zA-z-\d\s]+)$/;
168
201
  /**
@@ -268,6 +301,13 @@ function parseHints(ast) {
268
301
  .filter(notNullOrUndefined);
269
302
  }
270
303
 
304
+ /**
305
+ * Removes a variable's definition from the query (e.g. `$where: String` in the
306
+ * operation signature). Does not remove argument references to that variable in
307
+ * the query body.
308
+ *
309
+ * To remove both the definition and argument references, use `removeVariable`.
310
+ */
271
311
  function removeVariableDefinition(query, variableName) {
272
312
  return visit$1(query, {
273
313
  VariableDefinition: {
@@ -281,6 +321,14 @@ function removeVariableDefinition(query, variableName) {
281
321
  });
282
322
  }
283
323
 
324
+ /**
325
+ * Removes variable definitions from the specified operation when the variable
326
+ * does not have a corresponding value in the provided variables object (i.e.
327
+ * the value is `null` or `undefined`).
328
+ *
329
+ * Unlike `removeVariable`, this only removes the definitions — it does not
330
+ * remove argument references in the query body.
331
+ */
284
332
  function removeVariableDefinitionsNotDefined(query, node, variables) {
285
333
  return visit$1(query, {
286
334
  OperationDefinition(opDef) {
@@ -298,6 +346,14 @@ function removeVariableDefinitionsNotDefined(query, node, variables) {
298
346
  });
299
347
  }
300
348
 
349
+ /**
350
+ * Removes a variable from the query by removing both the variable definition
351
+ * (e.g. `$where: String` in the operation signature) and argument references
352
+ * with the same name (e.g. `where: $where` in the field arguments).
353
+ *
354
+ * To remove only the definition without touching arguments, use
355
+ * `removeVariableDefinition`.
356
+ */
301
357
  function removeVariable(query, variableName) {
302
358
  return visit$1(query, {
303
359
  VariableDefinition: {
@@ -468,70 +524,120 @@ const removeNotDefinedHintDef = {
468
524
  // ]
469
525
 
470
526
  /**
471
- * Apollo link that transforms GraphQL operations before they are sent.
527
+ * Transforms a GraphQL query and its variables according to the provided
528
+ * processing configuration.
472
529
  *
473
530
  * Two mechanisms are supported and can be combined freely:
474
531
  *
475
- * **Hint-based** — place `# @gql-hint: <name>` comments directly in the query.
476
- * Supported hints:
477
- * - `remove-not-defined` on the operation definition: removes every variable
478
- * whose value is null/undefined (definition + argument usage).
479
- * - `inline-variable` on a variable definition or usage: substitutes the
480
- * variable's current value directly into the query AST and removes it from
481
- * the variables map.
532
+ * **Hint-based** — `# @gql-hint: <name>` comments in the query.
533
+ * - `remove-not-defined`: removes variables whose value is null/undefined.
534
+ * - `inline-variable`: substitutes variable values directly into the AST.
482
535
  *
483
- * **Config-based** — pass a `QueryProcessingConfig` via Apollo context under
484
- * the key `queryProcessingConfig`. Supported options:
485
- * - `variables.removeIfNotDefined`: remove named variables when null/undefined.
486
- * - `variables.removeIfNotUsed`: remove named variable definitions when the
487
- * variable is not referenced anywhere in the (possibly already-transformed)
488
- * query body.
489
- * - `variables.inline`: inline named variables into the query AST.
536
+ * **Config-based** — via `QueryProcessingConfig`:
537
+ * - `removeIfNotDefined`: remove named variables when null/undefined.
538
+ * - `removeIfNotUsed`: remove named variable definitions when unreferenced.
539
+ * - `inline`: inline named variables into the query AST.
540
+ * - `orderTiebreaker`: append a fallback sort field for deterministic pagination.
490
541
  *
491
- * Hints are applied first, then config-based processing.
542
+ * Hints are applied first, then config-based processing, then cleanup.
492
543
  */
493
- const queryProcessingLink = new ApolloLink((operation, forward) => {
494
- const context = operation.getContext();
495
- const queryProcessingConfig = context.queryProcessingConfig ?? {};
496
- // Reparse to ensure token/comment info is present for hint parsing.
497
- let _ast = parseAst(operation.query);
544
+ function processGql(query, variables, queryProcessingConfig) {
545
+ let _ast = parseAst(query);
546
+ let _variables = { ...variables };
547
+ // ---- Hint: remove-not-defined ------------------------------------------
498
548
  const rules = parseHints(_ast);
499
- // ---- Hint: remove-not-defined ----------------------------------------
500
549
  for (const hintsToken of hintsTokensContainingHint(rules, removeNotDefinedHintDef.name)) {
501
- const result = removeNotDefinedHintDef.transformer({ query: _ast, variables: operation.variables }, hintsToken);
550
+ const result = removeNotDefinedHintDef.transformer({ query: _ast, variables: _variables }, hintsToken);
502
551
  _ast = result.query;
503
- operation.variables = result.variables;
552
+ _variables = result.variables;
504
553
  }
505
- // ---- Hint: inline-variable --------------------------------------------
554
+ // ---- Hint: inline-variable ---------------------------------------------
506
555
  for (const hintsToken of hintsTokensContainingHint(rules, inlineVariableHintDef.name)) {
507
- const result = inlineVariableHintDef.transformer({ query: _ast, variables: operation.variables }, hintsToken);
556
+ const result = inlineVariableHintDef.transformer({ query: _ast, variables: _variables }, hintsToken);
508
557
  _ast = result.query;
509
- operation.variables = result.variables;
558
+ _variables = result.variables;
510
559
  }
511
- // ---- Config: removeIfNotDefined ---------------------------------------
560
+ // ---- Config: removeIfNotDefined ----------------------------------------
512
561
  for (const varName of queryProcessingConfig?.variables?.removeIfNotDefined ??
513
562
  []) {
514
- if (isNullOrUndefined(operation.variables[varName])) {
563
+ if (isNullOrUndefined(_variables[varName])) {
515
564
  _ast = removeVariable(_ast, varName);
516
565
  }
517
566
  }
518
- // ---- Config: removeIfNotUsed -----------------------------------------
519
- // Intentionally runs after removeIfNotDefined so that variables which were
520
- // only referenced inside another (now-removed) variable can be cleaned up.
567
+ // ---- Config: removeIfNotUsed -------------------------------------------
521
568
  for (const varName of queryProcessingConfig?.variables?.removeIfNotUsed ??
522
569
  []) {
523
570
  if (!containsVariable(_ast, varName)) {
524
571
  _ast = removeVariable(_ast, varName);
525
572
  }
526
573
  }
527
- // ---- Config: inline --------------------------------------------------
574
+ // ---- Config: inline ----------------------------------------------------
528
575
  for (const varName of queryProcessingConfig?.variables?.inline ?? []) {
529
- const varValue = operation.variables[varName];
530
- operation.variables = withoutProperty(operation.variables, varName);
576
+ const varValue = _variables[varName];
577
+ _variables = withoutProperty(_variables, varName);
531
578
  _ast = removeVariableDefinition(_ast, varName);
532
579
  _ast = inlineVariable(_ast, varName, parseValue(toGQL(varValue)));
533
580
  }
534
- operation.query = _ast;
581
+ // ---- Config: orderTiebreaker -------------------------------------------
582
+ const orderTiebreaker = queryProcessingConfig?.variables?.orderTiebreaker;
583
+ if (typeof orderTiebreaker === 'string' && orderTiebreaker.length > 0) {
584
+ const order = _variables['order'];
585
+ if (!isNullOrUndefined(order) && Array.isArray(order)) {
586
+ const idx = order.findIndex((x) => x[orderTiebreaker] !== undefined);
587
+ if (idx === -1) {
588
+ _variables['order'] = [...order, { [orderTiebreaker]: 'DESC' }];
589
+ }
590
+ }
591
+ }
592
+ // ---- Cleanup: empty where ----------------------------------------------
593
+ // Targets inlined filter results like `where: { and: [] }` where the
594
+ // datatable's automated filter merging produced an empty array. We
595
+ // intentionally do NOT match deeper nesting (e.g. `where: { status: { in: [] } }`)
596
+ // — an empty condition at that level likely indicates a real bug in the
597
+ // filter code that should surface as an error.
598
+ _ast = visit$1(_ast, {
599
+ Argument(node) {
600
+ if (node.name.value === 'where' &&
601
+ node.value?.fields?.length &&
602
+ node.value?.fields[0]?.value?.values?.length === 0) {
603
+ return null;
604
+ }
605
+ },
606
+ });
607
+ return { query: _ast, variables: _variables };
608
+ }
609
+
610
+ /**
611
+ * Apollo link that transforms GraphQL operations before they are sent.
612
+ *
613
+ * Two mechanisms are supported and can be combined freely:
614
+ *
615
+ * **Hint-based** — place `# @gql-hint: <name>` comments directly in the query.
616
+ * Supported hints:
617
+ * - `remove-not-defined` on the operation definition: removes every variable
618
+ * whose value is null/undefined (definition + argument usage).
619
+ * - `inline-variable` on a variable definition or usage: substitutes the
620
+ * variable's current value directly into the query AST and removes it from
621
+ * the variables map.
622
+ *
623
+ * **Config-based** — pass a `QueryProcessingConfig` via Apollo context under
624
+ * the key `queryProcessingConfig`. Supported options:
625
+ * - `variables.removeIfNotDefined`: remove named variables when null/undefined.
626
+ * - `variables.removeIfNotUsed`: remove named variable definitions when the
627
+ * variable is not referenced anywhere in the (possibly already-transformed)
628
+ * query body.
629
+ * - `variables.inline`: inline named variables into the query AST.
630
+ * - `variables.orderTiebreaker`: append a fallback sort field for deterministic
631
+ * pagination.
632
+ *
633
+ * Hints are applied first, then config-based processing.
634
+ */
635
+ const queryProcessingLink = new ApolloLink((operation, forward) => {
636
+ const context = operation.getContext();
637
+ const queryProcessingConfig = context.queryProcessingConfig ?? {};
638
+ const result = processGql(operation.query, operation.variables, queryProcessingConfig);
639
+ operation.query = result.query;
640
+ operation.variables = result.variables;
535
641
  return forward(operation);
536
642
  });
537
643
 
@@ -990,6 +1096,20 @@ class DatatableGraphQLQueryRef {
990
1096
  // (including any disablePaging requery).
991
1097
  switchMap((v) => hasEmittedSubject.pipe(map((hasEmitted) => (hasEmitted ? v : true)))));
992
1098
  }
1099
+ /**
1100
+ * Returns an observable of mapped rows from the query result.
1101
+ *
1102
+ * The mapper transforms the raw GraphQL response data into the row format
1103
+ * the datatable expects, and provides the total count for pagination.
1104
+ *
1105
+ * @example
1106
+ * ```typescript
1107
+ * this.rows$ = this._queryRef.rows((data) => ({
1108
+ * rows: data.items.items,
1109
+ * totalCount: data.items.totalCount,
1110
+ * }))
1111
+ * ```
1112
+ */
993
1113
  rows(mapper) {
994
1114
  return this._rowsObservable(mapper);
995
1115
  }
@@ -1513,6 +1633,80 @@ const mapSearchTextColumnsDataFilterStateToGql = (filterState, context) => {
1513
1633
  return filter;
1514
1634
  };
1515
1635
 
1636
+ /**
1637
+ * Abstract transport for server-side datatable exports.
1638
+ *
1639
+ * THIS SHOULD BE IMPLEMENTED AND PROVIDED BY THE APPLICATION USING 'ui-common'.
1640
+ *
1641
+ * Example:
1642
+ * ```ts
1643
+ * bootstrapApplication(AppComponent, {
1644
+ * providers: [
1645
+ * provideDatatableExportTransport(AppDatatableExportTransport),
1646
+ * ],
1647
+ * })
1648
+ * ```
1649
+ */
1650
+ class DatatableExportTransport {
1651
+ }
1652
+ /**
1653
+ * Register the application's `DatatableExportTransport` implementation.
1654
+ *
1655
+ * ```ts
1656
+ * bootstrapApplication(AppComponent, {
1657
+ * providers: [
1658
+ * provideDatatableExportTransport(AppDatatableExportTransport),
1659
+ * ],
1660
+ * })
1661
+ * ```
1662
+ */
1663
+ function provideDatatableExportTransport(transportType) {
1664
+ return { provide: DatatableExportTransport, useClass: transportType };
1665
+ }
1666
+
1667
+ class DatatableGqlDataExporter {
1668
+ _exporterName;
1669
+ _exporterLabel;
1670
+ _columns;
1671
+ _queryRef;
1672
+ _transport;
1673
+ _exportType;
1674
+ get name() {
1675
+ return this._exporterName;
1676
+ }
1677
+ get label() {
1678
+ return this._exporterLabel;
1679
+ }
1680
+ icon = faFileCsv;
1681
+ skipDataMapping = true;
1682
+ constructor(_exporterName, _exporterLabel, _columns, _queryRef, _transport, _exportType) {
1683
+ this._exporterName = _exporterName;
1684
+ this._exporterLabel = _exporterLabel;
1685
+ this._columns = _columns;
1686
+ this._queryRef = _queryRef;
1687
+ this._transport = _transport;
1688
+ this._exportType = _exportType;
1689
+ }
1690
+ export(data) {
1691
+ const options = this._queryRef.getOptions();
1692
+ const { query, variables } = processGql(options.query, this._queryRef.getVariables(), this._queryRef.getQueryProcessingConfig() ?? { variables: {} });
1693
+ const operationName = query.definitions.find((d) => d.kind === 'OperationDefinition')?.name?.value ?? '';
1694
+ const payload = {
1695
+ graphQlQuery: {
1696
+ queryString: print(query),
1697
+ queryVariables: variables,
1698
+ operationName,
1699
+ columns: this._columns,
1700
+ },
1701
+ exportType: this._exportType,
1702
+ };
1703
+ return this._transport.export(payload).pipe(tap((file) => FileSaver.saveAs(file)), map(() => true), catchError((err) => {
1704
+ console.error(err);
1705
+ return of(false);
1706
+ }));
1707
+ }
1708
+ }
1709
+
1516
1710
  const baseSchemaFragment = gql `
1517
1711
  input ComparableInt32OperationFilterInput {
1518
1712
  eq: Int
@@ -2168,5 +2362,5 @@ const SIMPLE_GQL_TEST_SEARCH_QUERY = gql `
2168
2362
  * Generated bundle index. Do not edit.
2169
2363
  */
2170
2364
 
2171
- export { DATATABLE_GRAPHQL_SERVICE_CONFIG, DEFAULT_PAGE_SIZE, DEFAULT_TO_REMOVE_ON_UNDEFINED, DatatableGraphQLQueryRef, DatatableGraphqlService, GQLDirection, GQLVariable, HINT_NAMES_CAPTURE_REGEX, HINT_PREFIX_REGEX, HintsKind, MAX_ERROR_RECOVERY_ATTEMPTS, MockDatatable, SIMPLE_GQL_TEST_QUERY, SIMPLE_GQL_TEST_SEARCH_QUERY, baseSchemaFragment, checkRecordsHaveValue, containsVariable, createHintsToken, createMockApolloTestingProvider, createSimpleGqlTestRecord, createSimpleGqlTestRoot, createSortsMapper, filterWhere, filteredResults, getHintsToken, getPageInfo, getTokenAppliesTo, gqlVar, hintNamesFromHintToken, hintsTokensContainingHint, inlineVariable, inlineVariableHintDef, inlineVariableTransformer, isCommentToken, isHintToken, isInlineComment, logQueryLink, mapFilterStates, mapSearchDateColumnsDataFilterStateToGql, mapSearchNumericColumnsDataFilterStateToGql, mapSearchTextColumnsDataFilterStateToGql, mockGraphQLLink, observeRowsWithGqlInputsHandling, parseAst, parseComments, parseHints, queryProcessingLink, removeNotDefinedHintDef, removeNotDefinedTransformer, removeVariable, removeVariableDefinition, removeVariableDefinitionsNotDefined, skipAndTake, sortItems, toGQL };
2365
+ export { DATATABLE_GRAPHQL_SERVICE_CONFIG, DEFAULT_PAGE_SIZE, DEFAULT_TO_REMOVE_ON_UNDEFINED, DatatableExportTransport, DatatableGqlDataExporter, DatatableGraphQLQueryRef, DatatableGraphqlService, GQLDirection, GQLVariable, HINT_NAMES_CAPTURE_REGEX, HINT_PREFIX_REGEX, HintsKind, MAX_ERROR_RECOVERY_ATTEMPTS, MockDatatable, SIMPLE_GQL_TEST_QUERY, SIMPLE_GQL_TEST_SEARCH_QUERY, baseSchemaFragment, checkRecordsHaveValue, containsVariable, createHintsToken, createMockApolloTestingProvider, createSimpleGqlTestRecord, createSimpleGqlTestRoot, createSortsMapper, filterWhere, filteredResults, getHintsToken, getPageInfo, getTokenAppliesTo, gqlVar, hintNamesFromHintToken, hintsTokensContainingHint, inlineVariable, inlineVariableHintDef, inlineVariableTransformer, isCommentToken, isHintToken, isInlineComment, logQueryLink, mapFilterStates, mapSearchDateColumnsDataFilterStateToGql, mapSearchNumericColumnsDataFilterStateToGql, mapSearchTextColumnsDataFilterStateToGql, mockGraphQLLink, observeRowsWithGqlInputsHandling, parseAst, parseComments, parseHints, processGql, provideDatatableExportTransport, queryProcessingLink, removeNotDefinedHintDef, removeNotDefinedTransformer, removeVariable, removeVariableDefinition, removeVariableDefinitionsNotDefined, skipAndTake, sortItems, toGQL };
2172
2366
  //# sourceMappingURL=theseam-ui-common-graphql.mjs.map