@m1212e/rumble 0.27.3 → 0.27.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/out/index.cjs CHANGED
@@ -27,15 +27,15 @@ let es_toolkit = require("es-toolkit");
27
27
  let _opentelemetry_api = require("@opentelemetry/api");
28
28
  let _opentelemetry_semantic_conventions_incubating = require("@opentelemetry/semantic-conventions/incubating");
29
29
  let _pothos_tracing_opentelemetry = require("@pothos/tracing-opentelemetry");
30
+ let drizzle_orm_mysql_core = require("drizzle-orm/mysql-core");
31
+ let drizzle_orm_pg_core = require("drizzle-orm/pg-core");
32
+ let drizzle_orm_sqlite_core = require("drizzle-orm/sqlite-core");
30
33
  let _escape_tech_graphql_armor = require("@escape.tech/graphql-armor");
31
34
  let _graphql_yoga_plugin_disable_introspection = require("@graphql-yoga/plugin-disable-introspection");
32
35
  let graphql_yoga = require("graphql-yoga");
33
36
  let drizzle_orm_casing = require("drizzle-orm/casing");
34
- let drizzle_orm_pg_core = require("drizzle-orm/pg-core");
35
- let drizzle_orm_mysql_core = require("drizzle-orm/mysql-core");
36
37
  let pluralize = require("pluralize");
37
38
  pluralize = __toESM(pluralize, 1);
38
- let drizzle_orm_sqlite_core = require("drizzle-orm/sqlite-core");
39
39
  let _pothos_core = require("@pothos/core");
40
40
  _pothos_core = __toESM(_pothos_core, 1);
41
41
  let _pothos_plugin_drizzle = require("@pothos/plugin-drizzle");
@@ -201,6 +201,7 @@ const columnMaskKey = Symbol.for("rumble:columnMask");
201
201
  const columnVisibilityKey = Symbol.for("rumble:columnVisibility");
202
202
  const guardInputKey = Symbol.for("rumble:guardInput");
203
203
  const resolvedFilterKey = Symbol.for("rumble:resolvedFilter");
204
+ const refreshAbilitiesKey = Symbol.for("rumble:refreshAbilities");
204
205
  const requestStateKey = Symbol.for("rumble:requestState");
205
206
  // lib/abilityBuilder/requestState.ts
206
207
  function createRequestState() {
@@ -218,14 +219,6 @@ function requestStateOf(abilities) {
218
219
  return abilities?.[requestStateKey];
219
220
  }
220
221
  // lib/abilityBuilder/columnMask.ts
221
- function referencesRelation(filter, relations) {
222
- if (!filter || typeof filter !== "object") return false;
223
- return Object.entries(filter).some(([key, value]) => {
224
- if (key === "AND" || key === "OR") return value.some((v) => referencesRelation(v, relations));
225
- if (key === "NOT") return referencesRelation(value, relations);
226
- return key in relations;
227
- });
228
- }
229
222
  function selectedColumns(columns, allColumns) {
230
223
  if (!columns) return new Set(allColumns);
231
224
  const entries = Object.entries(columns).filter(([, v]) => v !== void 0);
@@ -284,12 +277,7 @@ function conditionalGroups(rules, guaranteed) {
284
277
  function createColumnAccessResolver({ db, actions }, tableSchema) {
285
278
  const allColumnNames = Object.keys(tableSchema.columns);
286
279
  const primaryKeyNames = Object.keys(tableSchema.primaryKey);
287
- const primaryKeyName = primaryKeyNames[0];
288
280
  const toSQL = (table, wheres) => (0, drizzle_orm.or)(...wheres.map((where) => (0, drizzle_orm.relationsFilterToSQL)(table, sanitizeFilterValue(where), tableSchema.relations, db._.relations)));
289
- const uncorrelated = (table, wheres, group) => {
290
- const inner = (0, drizzle_orm.aliasedTable)(tableSchema.table, `rumble_columns_${group}`);
291
- return drizzle_orm.sql`exists ${db.select({ pk: inner[primaryKeyName] }).from(inner).where(drizzle_orm.sql`${inner[primaryKeyName]} = ${table[primaryKeyName]} and (${toSQL(inner, wheres)})`)}`;
292
- };
293
281
  return function resolveColumnAccess(action, filters) {
294
282
  const rules = filters.map((f) => ({
295
283
  where: realWhere(f?.where),
@@ -320,11 +308,7 @@ function createColumnAccessResolver({ db, actions }, tableSchema) {
320
308
  const actionIndex = drizzle_orm.sql.raw(String(actions.indexOf(action)));
321
309
  const extras = { [ACTION_FLAG_KEY]: () => drizzle_orm.sql`${actionIndex}` };
322
310
  groups.forEach(({ wheres }, group) => {
323
- const relational = wheres.some((w) => referencesRelation(w, tableSchema.relations));
324
- extras[mask.flags[group]] = (table) => {
325
- const condition = relational ? uncorrelated(table, wheres, group) : toSQL(table, wheres);
326
- return drizzle_orm.sql`case when ${condition} then 1 else 0 end`;
327
- };
311
+ extras[mask.flags[group]] = (table) => drizzle_orm.sql`case when ${toSQL(table, wheres)} then 1 else 0 end`;
328
312
  });
329
313
  return {
330
314
  columns,
@@ -574,6 +558,7 @@ const ATTR_ABILITIES_STATUS = "rumble.abilities.status";
574
558
  const ATTR_ABILITIES_STATIC = "rumble.abilities.static";
575
559
  const ATTR_ABILITIES_DYNAMIC = "rumble.abilities.dynamic";
576
560
  const ATTR_ABILITIES_TOTAL = "rumble.abilities.total";
561
+ const ATTR_ABILITIES_RESOLVED = "rumble.abilities.resolved";
577
562
  const ATTR_FILTERS_TOTAL = "rumble.filters.total";
578
563
  const ATTR_FILTERS_ALLOWED = "rumble.filters.allowed";
579
564
  const ATTR_SUBSCRIPTION_EVENT_INDEX = "rumble.subscription.event_index";
@@ -796,6 +781,117 @@ function isDynamicQueryFilter(filter) {
796
781
  function isStaticQueryFilter(filter) {
797
782
  return typeof filter !== "function";
798
783
  }
784
+ // lib/helpers/determineDialectFromSchema.ts
785
+ const dialectCache = /* @__PURE__ */ new WeakMap();
786
+ function determineDBDialectFromSchema(schema) {
787
+ const cached = dialectCache.get(schema);
788
+ if (cached) return cached;
789
+ const found = /* @__PURE__ */ new Set();
790
+ for (const table of Object.values(schema).map((t) => t.table)) {
791
+ if (!table || typeof table !== "object") continue;
792
+ if ((0, drizzle_orm.is)(table, drizzle_orm_pg_core.PgTable)) found.add("postgres");
793
+ else if ((0, drizzle_orm.is)(table, drizzle_orm_mysql_core.MySqlTable)) found.add("mysql");
794
+ else if ((0, drizzle_orm.is)(table, drizzle_orm_sqlite_core.SQLiteTable)) found.add("sqlite");
795
+ }
796
+ const dialects = Array.from(found);
797
+ if (dialects.length === 1) {
798
+ const only = dialects[0];
799
+ dialectCache.set(schema, only);
800
+ return only;
801
+ }
802
+ if (dialects.length === 0) throw new Error("No tables found in schema, could not determine dialect");
803
+ throw new Error(`Multiple dialects found in schema: ${dialects.join(", ")}`);
804
+ }
805
+ function isPostgresDB(db) {
806
+ return determineDBDialectFromSchema(db._.relations) === "postgres";
807
+ }
808
+ // lib/abilityBuilder/resolveRules.ts
809
+ const DEFAULT_MAX_RESOLVED_IDS = 500;
810
+ /**
811
+ * Whether the where names a relation of the table, so it renders as (nested)
812
+ * `exists` subqueries.
813
+ */
814
+ function referencesRelation(filter, relations) {
815
+ if (!filter || typeof filter !== "object") return false;
816
+ return Object.entries(filter).some(([key, value]) => {
817
+ if (key === "AND" || key === "OR") return value.some((v) => referencesRelation(v, relations));
818
+ if (key === "NOT") return referencesRelation(value, relations);
819
+ return key in relations;
820
+ });
821
+ }
822
+ /**
823
+ * Creates the function replacing the relational rules of a table by the
824
+ * primary keys they match, resolved with one query per call.
825
+ *
826
+ * A relational rule ("the users whose delegation the caller supervises")
827
+ * renders as a chain of correlated `exists` subqueries, once in the row filter
828
+ * and once more in every column flag, at every place the table is nested in a
829
+ * query. Planning such a statement costs the database tens of megabytes. For a
830
+ * given caller the rule matches a fixed set of rows though, so this looks them
831
+ * up once (`select pk from table where <rule>`) and the rule becomes
832
+ * `pk in (...)`, which costs the planner nothing. Rules matching more than
833
+ * `maxIds` rows stay relational, so broad rules do not turn into huge lists.
834
+ *
835
+ * Returns undefined when disabled or the table has no single primary key.
836
+ */
837
+ function createRuleResolver(settings, tableSchema) {
838
+ const config = settings.resolveRelationalRules;
839
+ const primaryKeys = Object.keys(tableSchema.primaryKey);
840
+ if (!config?.enabled || primaryKeys.length !== 1) return void 0;
841
+ const maxIds = config.maxIds ?? DEFAULT_MAX_RESOLVED_IDS;
842
+ const primaryKeyName = primaryKeys[0];
843
+ const db = settings.db;
844
+ const table = tableSchema.table;
845
+ const dialect = determineDBDialectFromSchema(db._.relations);
846
+ const unionAll = {
847
+ postgres: drizzle_orm_pg_core.unionAll,
848
+ sqlite: drizzle_orm_sqlite_core.unionAll,
849
+ mysql: drizzle_orm_mysql_core.unionAll
850
+ }[dialect];
851
+ const listFilter = (ids) => dialect === "postgres" ? { RAW: (aliased) => drizzle_orm.sql`${aliased[primaryKeyName]} = any(${drizzle_orm.sql.param(ids)})` } : { [primaryKeyName]: { in: ids } };
852
+ /** The primary keys matching the rule, tagged with the rule's index. */
853
+ const matches = (where, rule) => {
854
+ const limited = db.select({
855
+ pk: table[primaryKeyName],
856
+ rule: drizzle_orm.sql.raw(String(rule)).as("rule")
857
+ }).from(table).where((0, drizzle_orm.relationsFilterToSQL)(table, sanitizeFilterValue(where), tableSchema.relations, db._.relations)).limit(maxIds + 1).as(`rumble_rule_${rule}`);
858
+ return db.select({
859
+ pk: limited.pk,
860
+ rule: limited.rule
861
+ }).from(limited);
862
+ };
863
+ const whereOf = (filter) => realWhere(filter?.where);
864
+ return async function resolveRules(filters) {
865
+ const relational = filters.flatMap((filter, index) => referencesRelation(whereOf(filter), tableSchema.relations) ? [index] : []);
866
+ if (relational.length === 0) return {
867
+ filters,
868
+ resolved: 0
869
+ };
870
+ const queries = relational.map((index) => matches(whereOf(filters[index]), index));
871
+ const rows = await (queries.length === 1 ? queries[0] : unionAll(...queries));
872
+ const idsPerRule = new Map(relational.map((index) => [index, []]));
873
+ for (const row of rows) idsPerRule.get(Number(row.rule)).push(row.pk);
874
+ let resolved = 0;
875
+ const result = [];
876
+ filters.forEach((filter, index) => {
877
+ const ids = idsPerRule.get(index);
878
+ if (!ids || ids.length > maxIds) {
879
+ result.push(filter);
880
+ return;
881
+ }
882
+ resolved++;
883
+ if (ids.length === 0) return;
884
+ result.push({
885
+ ...filter,
886
+ where: listFilter(ids)
887
+ });
888
+ });
889
+ return {
890
+ filters: result,
891
+ resolved
892
+ };
893
+ };
894
+ }
799
895
  // lib/helpers/errorLogging.ts
800
896
  function hasMessage(error) {
801
897
  return typeof error === "object" && error !== null && "message" in error;
@@ -933,7 +1029,23 @@ const createTableAbilities = (settings, tableName, queryFilters) => {
933
1029
  const blockEverythingFilter = createBlockEverythingFilter(String(tableName), tableSchema);
934
1030
  const resolveColumnAccess = createColumnAccessResolver(settings, tableSchema);
935
1031
  const transformToResponse = createFilterResponder(settings, tableSchema);
936
- const assembleAbilities = async (userContext, action, attributes) => {
1032
+ const resolveRules = createRuleResolver(settings, tableSchema);
1033
+ const evaluateDynamicFilters = (userContext, action) => Promise.all(dynamicQueryFilters[action].map((func) => func(userContext)));
1034
+ /** The response for the rules applying to the caller: their OR, columns and flags. */
1035
+ const respondWithFilters = (action, allQueryFilters) => {
1036
+ const mergedFilters = allQueryFilters.length === 1 ? { ...allQueryFilters[0] } : allQueryFilters.reduce((a, b) => {
1037
+ return mergeFilters(a, b, "OR");
1038
+ });
1039
+ const { columns, extras, mask, visibility } = resolveColumnAccess(action, allQueryFilters);
1040
+ if (columns) mergedFilters.columns = columns;
1041
+ else delete mergedFilters.columns;
1042
+ if (extras) mergedFilters.extras = {
1043
+ ...mergedFilters.extras,
1044
+ ...extras
1045
+ };
1046
+ return transformToResponse(mergedFilters, mask, visibility);
1047
+ };
1048
+ const assembleAbilities = async (action, attributes, evaluate) => {
937
1049
  const filters = queryFilters.get(action);
938
1050
  if (filters === "unrestricted") {
939
1051
  attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
@@ -944,7 +1056,7 @@ const createTableAbilities = (settings, tableName, queryFilters) => {
944
1056
  settings.warnNothingRegistered(String(tableName), action);
945
1057
  return transformToResponse(blockEverythingFilter);
946
1058
  }
947
- const rawResults = await Promise.all(dynamicQueryFilters[action].map((func) => func(userContext)));
1059
+ const rawResults = await evaluate();
948
1060
  if (rawResults.includes("allow")) {
949
1061
  attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
950
1062
  return transformToResponse();
@@ -952,28 +1064,32 @@ const createTableAbilities = (settings, tableName, queryFilters) => {
952
1064
  const dynamicResults = rawResults.filter((r) => r !== void 0).map(withRestrictingWhere);
953
1065
  attributes[ATTR_ABILITIES_DYNAMIC] = dynamicResults.length;
954
1066
  attributes[ATTR_ABILITIES_STATIC] = simpleQueryFilters[action].length;
955
- const allQueryFilters = [...simpleQueryFilters[action], ...dynamicResults];
1067
+ let allQueryFilters = [...simpleQueryFilters[action], ...dynamicResults];
956
1068
  attributes[ATTR_ABILITIES_TOTAL] = allQueryFilters.length;
1069
+ if (resolveRules && allQueryFilters.length > 0) {
1070
+ const resolution = await resolveRules(allQueryFilters);
1071
+ allQueryFilters = resolution.filters;
1072
+ attributes[ATTR_ABILITIES_RESOLVED] = resolution.resolved;
1073
+ }
957
1074
  if (allQueryFilters.length === 0) {
958
1075
  attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
959
1076
  return transformToResponse(blockEverythingFilter);
960
1077
  }
961
- const mergedFilters = allQueryFilters.length === 1 ? { ...allQueryFilters[0] } : allQueryFilters.reduce((a, b) => {
962
- return mergeFilters(a, b, "OR");
963
- });
964
- const { columns, extras, mask, visibility } = resolveColumnAccess(action, allQueryFilters);
965
- if (columns) mergedFilters.columns = columns;
966
- else delete mergedFilters.columns;
967
- if (extras) mergedFilters.extras = {
968
- ...mergedFilters.extras,
969
- ...extras
970
- };
971
1078
  attributes[ATTR_ABILITIES_STATUS] = "applied";
972
- return transformToResponse(mergedFilters, mask, visibility);
1079
+ return respondWithFilters(action, allQueryFilters);
973
1080
  };
974
1081
  return { withContext: (userContext) => {
975
- const prepare = (action) => traceAbilityPreparation(settings, String(tableName), action, (attributes) => assembleAbilities(userContext, action, attributes));
976
- const cache = /* @__PURE__ */ new Map();
1082
+ const evaluated = /* @__PURE__ */ new Map();
1083
+ const evaluate = (action) => {
1084
+ let results = evaluated.get(action);
1085
+ if (!results) {
1086
+ results = evaluateDynamicFilters(userContext, action);
1087
+ evaluated.set(action, results);
1088
+ }
1089
+ return results;
1090
+ };
1091
+ const prepare = (action) => traceAbilityPreparation(settings, String(tableName), action, (attributes) => assembleAbilities(action, attributes, () => evaluate(action)));
1092
+ let cache = /* @__PURE__ */ new Map();
977
1093
  const resolved = /* @__PURE__ */ new Map();
978
1094
  const abilities = { filter: (action) => {
979
1095
  let prepared = cache.get(action);
@@ -985,6 +1101,9 @@ const createTableAbilities = (settings, tableName, queryFilters) => {
985
1101
  return prepared;
986
1102
  } };
987
1103
  abilities[resolvedFilterKey] = (action) => resolved.get(action);
1104
+ abilities[refreshAbilitiesKey] = () => {
1105
+ if (resolveRules) cache = /* @__PURE__ */ new Map();
1106
+ };
988
1107
  return abilities;
989
1108
  } };
990
1109
  };
@@ -1205,8 +1324,20 @@ when: (queryFilter) => {
1205
1324
  }
1206
1325
  };
1207
1326
  };
1327
+ // lib/abilityBuilder/refresh.ts
1328
+ /**
1329
+ * Starts a new snapshot of the resolved relational rules of every table, see
1330
+ * `resolveRelationalRules`. Rumble calls this whenever a root field starts to
1331
+ * resolve. Call it yourself in a resolver that writes rows and then reads them
1332
+ * through a table whose rules it already resolved before the write. A no-op
1333
+ * unless rules are resolved.
1334
+ */
1335
+ function refreshAbilities(abilities) {
1336
+ if (!abilities) return;
1337
+ for (const table of Object.values(abilities)) table?.[refreshAbilitiesKey]?.();
1338
+ }
1208
1339
  // lib/abilityBuilder/index.ts
1209
- const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerConfig }) => {
1340
+ const createAbilityBuilder = ({ db, actions, defaultLimit, resolveRelationalRules, otel, logger: loggerConfig }) => {
1210
1341
  const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1211
1342
  const telemetryConfig = {
1212
1343
  otel,
@@ -1216,6 +1347,7 @@ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerC
1216
1347
  db,
1217
1348
  actions,
1218
1349
  defaultLimit,
1350
+ resolveRelationalRules,
1219
1351
  otel,
1220
1352
  telemetryConfig,
1221
1353
  log,
@@ -1424,7 +1556,7 @@ function mapNullFieldsToUndefined(obj) {
1424
1556
  return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, value === null ? void 0 : value]));
1425
1557
  }
1426
1558
  // package.json
1427
- var version = "0.27.3";
1559
+ var version = "0.27.4";
1428
1560
  // lib/args/orderArg.ts
1429
1561
  const makeDefaultName$1 = (dbName) => `${(0, es_toolkit.capitalize)((0, drizzle_orm_casing.toCamelCase)(dbName.toString()))}OrderInputArgument`;
1430
1562
  const createOrderArgImplementer = ({ db, schemaBuilder }) => {
@@ -2172,30 +2304,6 @@ function buildPothosResponseTypeFromGraphQLType({ builder, sqlType, fieldName, n
2172
2304
  default: throw new RumbleError(`Unsupported object type ${gqlType} for column ${fieldName}`);
2173
2305
  }
2174
2306
  }
2175
- // lib/helpers/determineDialectFromSchema.ts
2176
- const dialectCache = /* @__PURE__ */ new WeakMap();
2177
- function determineDBDialectFromSchema(schema) {
2178
- const cached = dialectCache.get(schema);
2179
- if (cached) return cached;
2180
- const found = /* @__PURE__ */ new Set();
2181
- for (const table of Object.values(schema).map((t) => t.table)) {
2182
- if (!table || typeof table !== "object") continue;
2183
- if ((0, drizzle_orm.is)(table, drizzle_orm_pg_core.PgTable)) found.add("postgres");
2184
- else if ((0, drizzle_orm.is)(table, drizzle_orm_mysql_core.MySqlTable)) found.add("mysql");
2185
- else if ((0, drizzle_orm.is)(table, drizzle_orm_sqlite_core.SQLiteTable)) found.add("sqlite");
2186
- }
2187
- const dialects = Array.from(found);
2188
- if (dialects.length === 1) {
2189
- const only = dialects[0];
2190
- dialectCache.set(schema, only);
2191
- return only;
2192
- }
2193
- if (dialects.length === 0) throw new Error("No tables found in schema, could not determine dialect");
2194
- throw new Error(`Multiple dialects found in schema: ${dialects.join(", ")}`);
2195
- }
2196
- function isPostgresDB(db) {
2197
- return determineDBDialectFromSchema(db._.relations) === "postgres";
2198
- }
2199
2307
  // lib/search.ts
2200
2308
  async function initSearchIfApplicable(input) {
2201
2309
  const log = input.logger?.enabled ? input.logger.logger : void 0;
@@ -3030,6 +3138,28 @@ function createAbilityAwareClient({ db, abilityBuilder }) {
3030
3138
  } });
3031
3139
  };
3032
3140
  }
3141
+ // lib/refreshAbilitiesPlugin/refreshAbilitiesPlugin.ts
3142
+ const refreshAbilitiesPluginName = "RefreshAbilitiesPlugin";
3143
+ /**
3144
+ * Starts a new snapshot of the resolved relational rules whenever a root
3145
+ * field (query, mutation or subscription event) starts to resolve, see
3146
+ * `resolveRelationalRules`. A mutation's second root field sees what the first
3147
+ * wrote, and a live query refetching sees the rows related to the caller by
3148
+ * now. A no-op unless rules are resolved.
3149
+ */
3150
+ var RefreshAbilitiesPlugin = class extends _pothos_core.BasePlugin {
3151
+ wrapResolve(resolver, fieldConfig) {
3152
+ const { kind } = this.buildCache.getTypeConfig(fieldConfig.parentType);
3153
+ if (kind !== "Query" && kind !== "Mutation" && kind !== "Subscription") return resolver;
3154
+ return (parent, args, context, info) => {
3155
+ refreshAbilities(context?.abilities);
3156
+ return resolver(parent, args, context, info);
3157
+ };
3158
+ }
3159
+ };
3160
+ function registerRefreshAbilitiesPlugin() {
3161
+ registerPluginOnce(refreshAbilitiesPluginName, RefreshAbilitiesPlugin);
3162
+ }
3033
3163
  // lib/runtimeFiltersPlugin/filterTypes.ts
3034
3164
  const pluginName = "RuntimeFiltersPlugin";
3035
3165
  // lib/runtimeFiltersPlugin/runtimeFiltersPlugin.ts
@@ -9199,10 +9329,12 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validatio
9199
9329
  const createSpan = otel?.enabled && otel.tracer ? (0, _pothos_tracing_opentelemetry.createOpenTelemetryWrapper)(otel.tracer, otel.options) : void 0;
9200
9330
  registerRuntimeFiltersPlugin();
9201
9331
  registerColumnMaskPlugin();
9332
+ registerRefreshAbilitiesPlugin();
9202
9333
  const schemaBuilder = new _pothos_core.default({
9203
9334
  ...pothosConfig,
9204
9335
  plugins: [
9205
9336
  columnMaskPluginName,
9337
+ refreshAbilitiesPluginName,
9206
9338
  pluginName,
9207
9339
  _pothos_plugin_drizzle.default,
9208
9340
  _pothos_plugin_smart_subscriptions.default,
@@ -9623,6 +9755,7 @@ exports.normalizeAddress = normalizeAddress;
9623
9755
  exports.normalizeEmailAddress = normalizeEmailAddress;
9624
9756
  exports.normalizePersonName = normalizePersonName;
9625
9757
  exports.parsePhoneNumber = parsePhoneNumber;
9758
+ exports.refreshAbilities = refreshAbilities;
9626
9759
  exports.rumble = rumble;
9627
9760
  exports.validateAddress = validateAddress;
9628
9761