@simtlix/simfinity-js 2.0.2 → 2.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.
- package/AGGREGATION_CHANGES_SUMMARY.md +235 -0
- package/AGGREGATION_EXAMPLE.md +567 -0
- package/README.md +415 -3
- package/package.json +1 -1
- package/src/index.js +345 -0
- package/src/scalars.js +188 -0
- package/src/validators.js +250 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -10
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -10
- package/.github/workflows/master.yml +0 -19
- package/.github/workflows/publish.yml +0 -45
- package/.github/workflows/release.yml +0 -65
- package/BACKUP_README.md +0 -26
- package/README_INDEX_EXAMPLE.md +0 -252
- package/simtlix-simfinity-js-1.9.1.tgz +0 -0
- package/tests/objectid-indexes.test.js +0 -215
- package/tests/prevent-collection-creation.test.js +0 -67
- package/tests/scalar-naming.test.js +0 -125
- package/tests/validated-scalar.test.js +0 -172
package/src/index.js
CHANGED
|
@@ -13,6 +13,41 @@ import QLSort from './const/QLSort.js';
|
|
|
13
13
|
|
|
14
14
|
mongoose.set('strictQuery', false);
|
|
15
15
|
|
|
16
|
+
// Custom JSON scalar type for aggregation results
|
|
17
|
+
const GraphQLJSON = new GraphQLScalarType({
|
|
18
|
+
name: 'JSON',
|
|
19
|
+
description: 'The `JSON` scalar type represents JSON values as specified by ECMA-404',
|
|
20
|
+
serialize(value) {
|
|
21
|
+
return value;
|
|
22
|
+
},
|
|
23
|
+
parseValue(value) {
|
|
24
|
+
return value;
|
|
25
|
+
},
|
|
26
|
+
parseLiteral(ast) {
|
|
27
|
+
switch (ast.kind) {
|
|
28
|
+
case Kind.STRING:
|
|
29
|
+
case Kind.BOOLEAN:
|
|
30
|
+
return ast.value;
|
|
31
|
+
case Kind.INT:
|
|
32
|
+
case Kind.FLOAT:
|
|
33
|
+
return parseFloat(ast.value);
|
|
34
|
+
case Kind.OBJECT: {
|
|
35
|
+
const value = Object.create(null);
|
|
36
|
+
ast.fields.forEach((field) => {
|
|
37
|
+
value[field.name.value] = GraphQLJSON.parseLiteral(field.value);
|
|
38
|
+
});
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
case Kind.LIST:
|
|
42
|
+
return ast.values.map((n) => GraphQLJSON.parseLiteral(n));
|
|
43
|
+
case Kind.NULL:
|
|
44
|
+
return null;
|
|
45
|
+
default:
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
16
51
|
// Adding 'extensions' field into instronspection query
|
|
17
52
|
const RelationType = new GraphQLObjectType({
|
|
18
53
|
name: 'RelationType',
|
|
@@ -147,6 +182,42 @@ const QLSortExpression = new GraphQLInputObjectType({
|
|
|
147
182
|
}),
|
|
148
183
|
});
|
|
149
184
|
|
|
185
|
+
const QLAggregationOperation = new GraphQLEnumType({
|
|
186
|
+
name: 'QLAggregationOperation',
|
|
187
|
+
values: {
|
|
188
|
+
SUM: { value: 'SUM' },
|
|
189
|
+
COUNT: { value: 'COUNT' },
|
|
190
|
+
AVG: { value: 'AVG' },
|
|
191
|
+
MIN: { value: 'MIN' },
|
|
192
|
+
MAX: { value: 'MAX' },
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const QLTypeAggregationFact = new GraphQLInputObjectType({
|
|
197
|
+
name: 'QLTypeAggregationFact',
|
|
198
|
+
fields: () => ({
|
|
199
|
+
operation: { type: new GraphQLNonNull(QLAggregationOperation) },
|
|
200
|
+
factName: { type: new GraphQLNonNull(GraphQLString) },
|
|
201
|
+
path: { type: new GraphQLNonNull(GraphQLString) },
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const QLTypeAggregationExpression = new GraphQLInputObjectType({
|
|
206
|
+
name: 'QLTypeAggregationExpression',
|
|
207
|
+
fields: () => ({
|
|
208
|
+
groupId: { type: new GraphQLNonNull(GraphQLString) },
|
|
209
|
+
facts: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(QLTypeAggregationFact))) },
|
|
210
|
+
}),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const QLTypeAggregationResult = new GraphQLObjectType({
|
|
214
|
+
name: 'QLTypeAggregationResult',
|
|
215
|
+
fields: () => ({
|
|
216
|
+
groupId: { type: GraphQLJSON },
|
|
217
|
+
facts: { type: GraphQLJSON },
|
|
218
|
+
}),
|
|
219
|
+
});
|
|
220
|
+
|
|
150
221
|
const isNonNullOfType = (fieldEntryType, graphQLType) => {
|
|
151
222
|
let isOfType = false;
|
|
152
223
|
if (fieldEntryType instanceof GraphQLNonNull) {
|
|
@@ -1482,6 +1553,254 @@ const buildQuery = async (input, gqltype, isCount) => {
|
|
|
1482
1553
|
return aggregateClauses;
|
|
1483
1554
|
};
|
|
1484
1555
|
|
|
1556
|
+
const buildFieldPath = (gqltype, fieldPath) => {
|
|
1557
|
+
// This function resolves a field path (e.g., "category" or "country.name")
|
|
1558
|
+
// and returns the MongoDB field path and any necessary lookups
|
|
1559
|
+
const pathParts = fieldPath.split('.');
|
|
1560
|
+
const aggregateClauses = [];
|
|
1561
|
+
let currentPath = '';
|
|
1562
|
+
let currentGQLType = gqltype;
|
|
1563
|
+
|
|
1564
|
+
for (let i = 0; i < pathParts.length; i++) {
|
|
1565
|
+
const part = pathParts[i];
|
|
1566
|
+
const field = currentGQLType.getFields()[part];
|
|
1567
|
+
|
|
1568
|
+
if (!field) {
|
|
1569
|
+
throw new Error(`Field ${part} not found in type ${currentGQLType.name}`);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
let fieldType = field.type;
|
|
1573
|
+
if (fieldType instanceof GraphQLNonNull || fieldType instanceof GraphQLList) {
|
|
1574
|
+
fieldType = fieldType.ofType;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// If it's an object type with non-embedded relation, we need a lookup
|
|
1578
|
+
if ((fieldType instanceof GraphQLObjectType) &&
|
|
1579
|
+
field.extensions && field.extensions.relation &&
|
|
1580
|
+
!field.extensions.relation.embedded) {
|
|
1581
|
+
|
|
1582
|
+
const relatedModel = typesDict.types[fieldType.name].model;
|
|
1583
|
+
const collectionName = relatedModel.collection.collectionName;
|
|
1584
|
+
const connectionField = field.extensions.relation.connectionField || part;
|
|
1585
|
+
|
|
1586
|
+
const lookupAlias = currentPath ? `${currentPath}_${part}` : part;
|
|
1587
|
+
const localField = currentPath ? `${currentPath}.${connectionField}` : connectionField;
|
|
1588
|
+
|
|
1589
|
+
aggregateClauses.push({
|
|
1590
|
+
$lookup: {
|
|
1591
|
+
from: collectionName,
|
|
1592
|
+
foreignField: '_id',
|
|
1593
|
+
localField,
|
|
1594
|
+
as: lookupAlias,
|
|
1595
|
+
},
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
aggregateClauses.push({
|
|
1599
|
+
$unwind: { path: `$${lookupAlias}`, preserveNullAndEmptyArrays: true },
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
currentPath = lookupAlias;
|
|
1603
|
+
currentGQLType = fieldType;
|
|
1604
|
+
} else if (fieldType instanceof GraphQLObjectType &&
|
|
1605
|
+
field.extensions && field.extensions.relation &&
|
|
1606
|
+
field.extensions.relation.embedded) {
|
|
1607
|
+
// Embedded object - just append to path
|
|
1608
|
+
currentPath = currentPath ? `${currentPath}.${part}` : part;
|
|
1609
|
+
currentGQLType = fieldType;
|
|
1610
|
+
} else {
|
|
1611
|
+
// Scalar field - final part of path
|
|
1612
|
+
if (part === 'id') {
|
|
1613
|
+
currentPath = currentPath ? `${currentPath}._id` : '_id';
|
|
1614
|
+
} else {
|
|
1615
|
+
currentPath = currentPath ? `${currentPath}.${part}` : part;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
return { mongoPath: currentPath, lookups: aggregateClauses };
|
|
1621
|
+
};
|
|
1622
|
+
|
|
1623
|
+
const buildAggregationQuery = async (input, gqltype, aggregationExpression) => {
|
|
1624
|
+
const aggregateClauses = [];
|
|
1625
|
+
const matchesClauses = { $match: {} };
|
|
1626
|
+
let addMatch = false;
|
|
1627
|
+
const aggregationsIncluded = {};
|
|
1628
|
+
const sortTerms = []; // Store multiple sort terms
|
|
1629
|
+
let limitClause = null;
|
|
1630
|
+
let skipClause = null;
|
|
1631
|
+
|
|
1632
|
+
// Build filter and lookup clauses (similar to buildQuery)
|
|
1633
|
+
for (const [key, filterField] of Object.entries(input)) {
|
|
1634
|
+
if (Object.prototype.hasOwnProperty.call(input, key) && key !== 'pagination' && key !== 'sort' && key !== 'aggregation') {
|
|
1635
|
+
const qlField = gqltype.getFields()[key];
|
|
1636
|
+
|
|
1637
|
+
const result = await buildQueryTerms(filterField, qlField, key);
|
|
1638
|
+
|
|
1639
|
+
if (result) {
|
|
1640
|
+
for (const [prop, aggregate] of Object.entries(result.aggregateClauses)) {
|
|
1641
|
+
aggregateClauses.push(aggregate.lookup);
|
|
1642
|
+
aggregateClauses.push(aggregate.unwind);
|
|
1643
|
+
aggregationsIncluded[prop] = true;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
for (const [matchClauseKey, matchClause] of Object.entries(result.matchesClauses)) {
|
|
1647
|
+
if (Object.prototype.hasOwnProperty.call(result.matchesClauses, matchClauseKey)) {
|
|
1648
|
+
for (const [matchKey, match] of Object.entries(matchClause)) {
|
|
1649
|
+
if (Object.prototype.hasOwnProperty.call(matchClause, matchKey)) {
|
|
1650
|
+
matchesClauses.$match[matchKey] = match;
|
|
1651
|
+
addMatch = true;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
} else if (key === 'sort' && filterField && filterField.terms && filterField.terms.length > 0) {
|
|
1658
|
+
// Extract all sort terms
|
|
1659
|
+
filterField.terms.forEach(sortTerm => {
|
|
1660
|
+
sortTerms.push({
|
|
1661
|
+
field: sortTerm.field || 'groupId',
|
|
1662
|
+
direction: sortTerm.order === 'ASC' ? 1 : -1,
|
|
1663
|
+
});
|
|
1664
|
+
});
|
|
1665
|
+
} else if (key === 'pagination' && filterField) {
|
|
1666
|
+
// Handle pagination (ignore count parameter)
|
|
1667
|
+
if (filterField.page && filterField.size) {
|
|
1668
|
+
const skip = filterField.size * (filterField.page - 1);
|
|
1669
|
+
limitClause = { $limit: filterField.size + skip };
|
|
1670
|
+
skipClause = { $skip: skip };
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
if (addMatch) {
|
|
1676
|
+
aggregateClauses.push(matchesClauses);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// Now build the aggregation with $group
|
|
1680
|
+
const { groupId, facts } = aggregationExpression;
|
|
1681
|
+
|
|
1682
|
+
// Resolve the groupId field path
|
|
1683
|
+
const groupIdPath = buildFieldPath(gqltype, groupId);
|
|
1684
|
+
|
|
1685
|
+
// Add any lookups needed for the groupId field
|
|
1686
|
+
groupIdPath.lookups.forEach(lookup => {
|
|
1687
|
+
const lookupKey = Object.keys(lookup)[0];
|
|
1688
|
+
const lookupAlias = lookup[lookupKey].as;
|
|
1689
|
+
if (!aggregationsIncluded[lookupAlias]) {
|
|
1690
|
+
aggregateClauses.push(lookup);
|
|
1691
|
+
// Check if next item is an unwind for this lookup
|
|
1692
|
+
const unwindItem = groupIdPath.lookups[groupIdPath.lookups.indexOf(lookup) + 1];
|
|
1693
|
+
if (unwindItem && unwindItem.$unwind) {
|
|
1694
|
+
aggregateClauses.push(unwindItem);
|
|
1695
|
+
}
|
|
1696
|
+
aggregationsIncluded[lookupAlias] = true;
|
|
1697
|
+
}
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
// Build the $group stage
|
|
1701
|
+
const groupStage = {
|
|
1702
|
+
$group: {
|
|
1703
|
+
_id: `$${groupIdPath.mongoPath}`,
|
|
1704
|
+
},
|
|
1705
|
+
};
|
|
1706
|
+
|
|
1707
|
+
// Add aggregation operations for each fact
|
|
1708
|
+
facts.forEach(fact => {
|
|
1709
|
+
const { operation, factName, path } = fact;
|
|
1710
|
+
const factPath = buildFieldPath(gqltype, path);
|
|
1711
|
+
|
|
1712
|
+
// Add any lookups needed for the fact field
|
|
1713
|
+
factPath.lookups.forEach(lookup => {
|
|
1714
|
+
const lookupKey = Object.keys(lookup)[0];
|
|
1715
|
+
const lookupAlias = lookup[lookupKey].as;
|
|
1716
|
+
if (!aggregationsIncluded[lookupAlias]) {
|
|
1717
|
+
aggregateClauses.push(lookup);
|
|
1718
|
+
// Check if next item is an unwind for this lookup
|
|
1719
|
+
const unwindItem = factPath.lookups[factPath.lookups.indexOf(lookup) + 1];
|
|
1720
|
+
if (unwindItem && unwindItem.$unwind) {
|
|
1721
|
+
aggregateClauses.push(unwindItem);
|
|
1722
|
+
}
|
|
1723
|
+
aggregationsIncluded[lookupAlias] = true;
|
|
1724
|
+
}
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
// Map GraphQL operations to MongoDB aggregation operators
|
|
1728
|
+
let mongoOperation;
|
|
1729
|
+
switch (operation) {
|
|
1730
|
+
case 'SUM':
|
|
1731
|
+
mongoOperation = { $sum: `$${factPath.mongoPath}` };
|
|
1732
|
+
break;
|
|
1733
|
+
case 'COUNT':
|
|
1734
|
+
mongoOperation = { $sum: 1 };
|
|
1735
|
+
break;
|
|
1736
|
+
case 'AVG':
|
|
1737
|
+
mongoOperation = { $avg: `$${factPath.mongoPath}` };
|
|
1738
|
+
break;
|
|
1739
|
+
case 'MIN':
|
|
1740
|
+
mongoOperation = { $min: `$${factPath.mongoPath}` };
|
|
1741
|
+
break;
|
|
1742
|
+
case 'MAX':
|
|
1743
|
+
mongoOperation = { $max: `$${factPath.mongoPath}` };
|
|
1744
|
+
break;
|
|
1745
|
+
default:
|
|
1746
|
+
throw new Error(`Unknown aggregation operation: ${operation}`);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
groupStage.$group[factName] = mongoOperation;
|
|
1750
|
+
});
|
|
1751
|
+
|
|
1752
|
+
aggregateClauses.push(groupStage);
|
|
1753
|
+
|
|
1754
|
+
// Add a final projection stage to format the output
|
|
1755
|
+
aggregateClauses.push({
|
|
1756
|
+
$project: {
|
|
1757
|
+
_id: 0,
|
|
1758
|
+
groupId: '$_id',
|
|
1759
|
+
facts: Object.fromEntries(facts.map(fact => [fact.factName, `$${fact.factName}`])),
|
|
1760
|
+
},
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
// Build sort object from multiple sort terms
|
|
1764
|
+
if (sortTerms.length > 0) {
|
|
1765
|
+
const sortObject = {};
|
|
1766
|
+
const factNames = facts.map(fact => fact.factName);
|
|
1767
|
+
|
|
1768
|
+
sortTerms.forEach(sortTerm => {
|
|
1769
|
+
let sortFieldPath = 'groupId';
|
|
1770
|
+
|
|
1771
|
+
if (sortTerm.field !== 'groupId') {
|
|
1772
|
+
// Check if the field is one of the fact names
|
|
1773
|
+
if (factNames.includes(sortTerm.field)) {
|
|
1774
|
+
sortFieldPath = `facts.${sortTerm.field}`;
|
|
1775
|
+
}
|
|
1776
|
+
// If not found, default to groupId (already set)
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
sortObject[sortFieldPath] = sortTerm.direction;
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
// Add sort stage with all sort fields
|
|
1783
|
+
aggregateClauses.push({
|
|
1784
|
+
$sort: sortObject,
|
|
1785
|
+
});
|
|
1786
|
+
} else {
|
|
1787
|
+
// Default sort by groupId ascending if no sort terms provided
|
|
1788
|
+
aggregateClauses.push({
|
|
1789
|
+
$sort: { groupId: 1 },
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// Add pagination if provided
|
|
1794
|
+
if (limitClause) {
|
|
1795
|
+
aggregateClauses.push(limitClause);
|
|
1796
|
+
}
|
|
1797
|
+
if (skipClause) {
|
|
1798
|
+
aggregateClauses.push(skipClause);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
return aggregateClauses;
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1485
1804
|
const buildRootQuery = (name, includedTypes) => {
|
|
1486
1805
|
const rootQueryArgs = {};
|
|
1487
1806
|
rootQueryArgs.name = name;
|
|
@@ -1545,6 +1864,29 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1545
1864
|
return result;
|
|
1546
1865
|
},
|
|
1547
1866
|
};
|
|
1867
|
+
|
|
1868
|
+
// Add aggregate endpoint
|
|
1869
|
+
const aggregateArgsObject = { ...argsObject };
|
|
1870
|
+
aggregateArgsObject.aggregation = {
|
|
1871
|
+
type: new GraphQLNonNull(QLTypeAggregationExpression),
|
|
1872
|
+
};
|
|
1873
|
+
|
|
1874
|
+
rootQueryArgs.fields[`${type.listEntitiesEndpointName}_aggregate`] = {
|
|
1875
|
+
type: new GraphQLList(QLTypeAggregationResult),
|
|
1876
|
+
args: aggregateArgsObject,
|
|
1877
|
+
async resolve(parent, args, context) {
|
|
1878
|
+
const params = {
|
|
1879
|
+
type,
|
|
1880
|
+
args,
|
|
1881
|
+
operation: 'aggregate',
|
|
1882
|
+
context,
|
|
1883
|
+
};
|
|
1884
|
+
excecuteMiddleware(params);
|
|
1885
|
+
const aggregateClauses = await buildAggregationQuery(args, type.gqltype, args.aggregation);
|
|
1886
|
+
const result = await type.model.aggregate(aggregateClauses);
|
|
1887
|
+
return result;
|
|
1888
|
+
},
|
|
1889
|
+
};
|
|
1548
1890
|
}
|
|
1549
1891
|
}
|
|
1550
1892
|
}
|
|
@@ -1733,6 +2075,9 @@ export const addNoEndpointType = (gqltype) => {
|
|
|
1733
2075
|
|
|
1734
2076
|
export { createValidatedScalar };
|
|
1735
2077
|
|
|
2078
|
+
export { default as validators } from './validators.js';
|
|
2079
|
+
export { default as scalars } from './scalars.js';
|
|
2080
|
+
|
|
1736
2081
|
const createArgsForQuery = (argTypes) => {
|
|
1737
2082
|
const argsObject = {};
|
|
1738
2083
|
|
package/src/scalars.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GraphQLString, GraphQLInt, GraphQLFloat,
|
|
3
|
+
} from 'graphql';
|
|
4
|
+
import { createValidatedScalar } from './index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Email scalar - validates email format
|
|
8
|
+
* Type name: Email_String
|
|
9
|
+
*/
|
|
10
|
+
export const EmailScalar = createValidatedScalar(
|
|
11
|
+
'Email',
|
|
12
|
+
'A valid email address',
|
|
13
|
+
GraphQLString,
|
|
14
|
+
(value) => {
|
|
15
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
16
|
+
if (!emailRegex.test(value)) {
|
|
17
|
+
throw new Error('Invalid email format');
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* URL scalar - validates URL format
|
|
24
|
+
* Type name: URL_String
|
|
25
|
+
*/
|
|
26
|
+
export const URLScalar = createValidatedScalar(
|
|
27
|
+
'URL',
|
|
28
|
+
'A valid URL',
|
|
29
|
+
GraphQLString,
|
|
30
|
+
(value) => {
|
|
31
|
+
try {
|
|
32
|
+
new URL(value);
|
|
33
|
+
} catch {
|
|
34
|
+
throw new Error('Invalid URL format');
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* PositiveInt scalar - validates positive integers
|
|
41
|
+
* Type name: PositiveInt_Int
|
|
42
|
+
*/
|
|
43
|
+
export const PositiveIntScalar = createValidatedScalar(
|
|
44
|
+
'PositiveInt',
|
|
45
|
+
'A positive integer',
|
|
46
|
+
GraphQLInt,
|
|
47
|
+
(value) => {
|
|
48
|
+
if (value <= 0) {
|
|
49
|
+
throw new Error('Value must be positive');
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* PositiveFloat scalar - validates positive floats
|
|
56
|
+
* Type name: PositiveFloat_Float
|
|
57
|
+
*/
|
|
58
|
+
export const PositiveFloatScalar = createValidatedScalar(
|
|
59
|
+
'PositiveFloat',
|
|
60
|
+
'A positive float',
|
|
61
|
+
GraphQLFloat,
|
|
62
|
+
(value) => {
|
|
63
|
+
if (value <= 0) {
|
|
64
|
+
throw new Error('Value must be positive');
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Factory function to create a bounded string scalar
|
|
71
|
+
* @param {string} name - Name for the scalar
|
|
72
|
+
* @param {number} min - Minimum length
|
|
73
|
+
* @param {number} max - Maximum length
|
|
74
|
+
* @returns {GraphQLScalarType} A scalar type with length validation
|
|
75
|
+
*/
|
|
76
|
+
export const createBoundedStringScalar = (name, min, max) => {
|
|
77
|
+
return createValidatedScalar(
|
|
78
|
+
name,
|
|
79
|
+
`A string with length between ${min} and ${max} characters`,
|
|
80
|
+
GraphQLString,
|
|
81
|
+
(value) => {
|
|
82
|
+
if (typeof value !== 'string') {
|
|
83
|
+
throw new Error('Value must be a string');
|
|
84
|
+
}
|
|
85
|
+
if (min !== undefined && value.length < min) {
|
|
86
|
+
throw new Error(`String must be at least ${min} characters`);
|
|
87
|
+
}
|
|
88
|
+
if (max !== undefined && value.length > max) {
|
|
89
|
+
throw new Error(`String must be at most ${max} characters`);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Factory function to create a bounded integer scalar
|
|
97
|
+
* @param {string} name - Name for the scalar
|
|
98
|
+
* @param {number} min - Minimum value
|
|
99
|
+
* @param {number} max - Maximum value
|
|
100
|
+
* @returns {GraphQLScalarType} A scalar type with range validation
|
|
101
|
+
*/
|
|
102
|
+
export const createBoundedIntScalar = (name, min, max) => {
|
|
103
|
+
return createValidatedScalar(
|
|
104
|
+
name,
|
|
105
|
+
`An integer between ${min} and ${max}`,
|
|
106
|
+
GraphQLInt,
|
|
107
|
+
(value) => {
|
|
108
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
109
|
+
throw new Error('Value must be a number');
|
|
110
|
+
}
|
|
111
|
+
if (min !== undefined && value < min) {
|
|
112
|
+
throw new Error(`Value must be at least ${min}`);
|
|
113
|
+
}
|
|
114
|
+
if (max !== undefined && value > max) {
|
|
115
|
+
throw new Error(`Value must be at most ${max}`);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Factory function to create a bounded float scalar
|
|
123
|
+
* @param {string} name - Name for the scalar
|
|
124
|
+
* @param {number} min - Minimum value
|
|
125
|
+
* @param {number} max - Maximum value
|
|
126
|
+
* @returns {GraphQLScalarType} A scalar type with range validation
|
|
127
|
+
*/
|
|
128
|
+
export const createBoundedFloatScalar = (name, min, max) => {
|
|
129
|
+
return createValidatedScalar(
|
|
130
|
+
name,
|
|
131
|
+
`A float between ${min} and ${max}`,
|
|
132
|
+
GraphQLFloat,
|
|
133
|
+
(value) => {
|
|
134
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
135
|
+
throw new Error('Value must be a number');
|
|
136
|
+
}
|
|
137
|
+
if (min !== undefined && value < min) {
|
|
138
|
+
throw new Error(`Value must be at least ${min}`);
|
|
139
|
+
}
|
|
140
|
+
if (max !== undefined && value > max) {
|
|
141
|
+
throw new Error(`Value must be at most ${max}`);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Factory function to create a regex pattern string scalar
|
|
149
|
+
* @param {string} name - Name for the scalar
|
|
150
|
+
* @param {RegExp|string} pattern - Regex pattern to validate against
|
|
151
|
+
* @param {string} message - Error message if validation fails
|
|
152
|
+
* @returns {GraphQLScalarType} A scalar type with pattern validation
|
|
153
|
+
*/
|
|
154
|
+
export const createPatternStringScalar = (name, pattern, message) => {
|
|
155
|
+
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
|
156
|
+
const errorMessage = message || 'Value does not match required pattern';
|
|
157
|
+
|
|
158
|
+
return createValidatedScalar(
|
|
159
|
+
name,
|
|
160
|
+
`A string matching the pattern: ${pattern}`,
|
|
161
|
+
GraphQLString,
|
|
162
|
+
(value) => {
|
|
163
|
+
if (typeof value !== 'string') {
|
|
164
|
+
throw new Error('Value must be a string');
|
|
165
|
+
}
|
|
166
|
+
if (!regex.test(value)) {
|
|
167
|
+
throw new Error(errorMessage);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Export all scalars as an object for convenience
|
|
174
|
+
const scalars = {
|
|
175
|
+
// Pre-built scalars
|
|
176
|
+
EmailScalar,
|
|
177
|
+
URLScalar,
|
|
178
|
+
PositiveIntScalar,
|
|
179
|
+
PositiveFloatScalar,
|
|
180
|
+
// Factory functions
|
|
181
|
+
createBoundedStringScalar,
|
|
182
|
+
createBoundedIntScalar,
|
|
183
|
+
createBoundedFloatScalar,
|
|
184
|
+
createPatternStringScalar,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export default scalars;
|
|
188
|
+
|