polen 0.11.0-next.16 → 0.11.0-next.17

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.
Files changed (93) hide show
  1. package/build/api/examples/diagnostic/diagnostic.d.ts +4 -4
  2. package/build/api/examples/diagnostic/missing-versions.d.ts +5 -4
  3. package/build/api/examples/diagnostic/missing-versions.d.ts.map +1 -1
  4. package/build/api/examples/diagnostic/missing-versions.js +3 -2
  5. package/build/api/examples/diagnostic/missing-versions.js.map +1 -1
  6. package/build/api/examples/diagnostic/unknown-version.d.ts +5 -4
  7. package/build/api/examples/diagnostic/unknown-version.d.ts.map +1 -1
  8. package/build/api/examples/diagnostic/unknown-version.js +3 -2
  9. package/build/api/examples/diagnostic/unknown-version.js.map +1 -1
  10. package/build/api/examples/diagnostic/validator.d.ts.map +1 -1
  11. package/build/api/examples/diagnostic/validator.js +7 -8
  12. package/build/api/examples/diagnostic/validator.js.map +1 -1
  13. package/build/api/examples/scanner.d.ts.map +1 -1
  14. package/build/api/examples/scanner.js +20 -16
  15. package/build/api/examples/scanner.js.map +1 -1
  16. package/build/api/examples/type-usage-indexer.d.ts.map +1 -1
  17. package/build/api/examples/type-usage-indexer.js +4 -5
  18. package/build/api/examples/type-usage-indexer.js.map +1 -1
  19. package/build/api/schema/input-sources/versioned-directory.d.ts +3 -3
  20. package/build/api/schema/input-sources/versioned-directory.d.ts.map +1 -1
  21. package/build/api/schema/input-sources/versioned-directory.js +4 -2
  22. package/build/api/schema/input-sources/versioned-directory.js.map +1 -1
  23. package/build/api/schema/load.js +1 -1
  24. package/build/api/schema/load.js.map +1 -1
  25. package/build/cli/commands/hero-image.js +1 -1
  26. package/build/cli/commands/hero-image.js.map +1 -1
  27. package/build/lib/catalog/catalog.d.ts +24 -23
  28. package/build/lib/catalog/catalog.d.ts.map +1 -1
  29. package/build/lib/catalog/catalog.js +7 -13
  30. package/build/lib/catalog/catalog.js.map +1 -1
  31. package/build/lib/catalog/versioned.d.ts +36 -26
  32. package/build/lib/catalog/versioned.d.ts.map +1 -1
  33. package/build/lib/catalog/versioned.js +25 -6
  34. package/build/lib/catalog/versioned.js.map +1 -1
  35. package/build/lib/catalog-statistics/analyze-catalog.js +3 -3
  36. package/build/lib/catalog-statistics/analyze-catalog.js.map +1 -1
  37. package/build/lib/lifecycles/lifecycles.js +1 -1
  38. package/build/lib/lifecycles/lifecycles.js.map +1 -1
  39. package/build/template/components/Changelog/Changelog.d.ts.map +1 -1
  40. package/build/template/components/Changelog/Changelog.js +7 -7
  41. package/build/template/components/Changelog/Changelog.js.map +1 -1
  42. package/build/template/components/GraphQLDocument.js +6 -5
  43. package/build/template/components/GraphQLDocument.js.map +1 -1
  44. package/build/template/components/VersionPicker.d.ts.map +1 -1
  45. package/build/template/components/VersionPicker.js +5 -2
  46. package/build/template/components/VersionPicker.js.map +1 -1
  47. package/build/template/components/home/FeaturesGrid.js +1 -1
  48. package/build/template/components/home/FeaturesGrid.js.map +1 -1
  49. package/build/template/components/home/HeroSection.js +1 -1
  50. package/build/template/components/home/HeroSection.js.map +1 -1
  51. package/build/template/components/home/RecentChanges.d.ts.map +1 -1
  52. package/build/template/components/home/RecentChanges.js +2 -1
  53. package/build/template/components/home/RecentChanges.js.map +1 -1
  54. package/build/template/routes/changelog.d.ts +1 -1
  55. package/build/template/routes/changelog.d.ts.map +1 -1
  56. package/build/template/routes/changelog.js +7 -4
  57. package/build/template/routes/changelog.js.map +1 -1
  58. package/build/template/routes/examples/_index.js +1 -1
  59. package/build/template/routes/examples/_index.js.map +1 -1
  60. package/build/template/routes/reference.js +6 -6
  61. package/build/template/routes/reference.js.map +1 -1
  62. package/build/vite/plugins/navbar.js +1 -1
  63. package/build/vite/plugins/navbar.js.map +1 -1
  64. package/build/vite/plugins/routes-manifest.js +1 -1
  65. package/build/vite/plugins/routes-manifest.js.map +1 -1
  66. package/package.json +1 -1
  67. package/src/api/examples/diagnostic/missing-versions.ts +3 -2
  68. package/src/api/examples/diagnostic/unknown-version.ts +3 -2
  69. package/src/api/examples/diagnostic/validator.test.ts +70 -55
  70. package/src/api/examples/diagnostic/validator.ts +7 -9
  71. package/src/api/examples/scanner.ts +25 -20
  72. package/src/api/examples/type-usage-indexer.test.ts +44 -33
  73. package/src/api/examples/type-usage-indexer.ts +4 -7
  74. package/src/api/schema/$.test.ts +2 -2
  75. package/src/api/schema/input-sources/versioned-directory.ts +7 -2
  76. package/src/api/schema/load.ts +1 -1
  77. package/src/cli/commands/hero-image.ts +1 -1
  78. package/src/lib/catalog/catalog.ts +7 -13
  79. package/src/lib/catalog/versioned.ts +35 -6
  80. package/src/lib/catalog-statistics/$.test.ts +22 -12
  81. package/src/lib/catalog-statistics/analyze-catalog.ts +3 -3
  82. package/src/lib/lifecycles/lifecycles.ts +1 -1
  83. package/src/template/components/Changelog/Changelog.tsx +10 -6
  84. package/src/template/components/GraphQLDocument.tsx +6 -5
  85. package/src/template/components/VersionPicker.tsx +9 -2
  86. package/src/template/components/home/FeaturesGrid.tsx +1 -1
  87. package/src/template/components/home/HeroSection.tsx +1 -1
  88. package/src/template/components/home/RecentChanges.tsx +3 -1
  89. package/src/template/routes/changelog.tsx +10 -4
  90. package/src/template/routes/examples/_index.tsx +1 -1
  91. package/src/template/routes/reference.tsx +6 -6
  92. package/src/vite/plugins/navbar.ts +1 -1
  93. package/src/vite/plugins/routes-manifest.ts +1 -1
@@ -1,8 +1,8 @@
1
1
  import { Catalog as SchemaCatalog } from '#lib/catalog/$'
2
2
  import { Document } from '#lib/document/$'
3
3
  import { Version } from '#lib/version/$'
4
- import { HashMap, Match, Option } from 'effect'
5
- import { Array } from 'effect'
4
+ import { Match } from 'effect'
5
+ import { HashMap } from 'effect/Schema'
6
6
  import type { GraphQLError, GraphQLSchema } from 'graphql'
7
7
  import { parse, specifiedRules, validate } from 'graphql'
8
8
  import { Example } from '../schemas/example/$.js'
@@ -35,23 +35,21 @@ export const validateExamples = (
35
35
  return Match.value(catalog).pipe(
36
36
  Match.tagsExhaustive({
37
37
  CatalogVersioned: (versioned) => {
38
- const schemaVersions = versioned.entries.map(e => e.version)
39
-
40
38
  for (const example of examples) {
41
39
  Match.value(example.document).pipe(
42
40
  Match.tagsExhaustive({
43
41
  DocumentVersioned: (doc) => {
44
42
  // Validate each version against its corresponding schema
45
- for (const entry of versioned.entries) {
46
- const content = Document.Versioned.getContentForVersion(doc, entry.version)
43
+ for (const schema of SchemaCatalog.Versioned.getAll(versioned)) {
44
+ const content = Document.Versioned.getContentForVersion(doc, schema.version)
47
45
  if (content) {
48
- const versionStr = Version.encodeSync(entry.version)
46
+ const versionStr = Version.encodeSync(schema.version)
49
47
  validateDocument(
50
48
  example.name,
51
49
  example.path,
52
50
  versionStr,
53
51
  content,
54
- entry.definition,
52
+ schema.definition,
55
53
  diagnostics,
56
54
  )
57
55
  }
@@ -59,7 +57,7 @@ export const validateExamples = (
59
57
  return undefined
60
58
  },
61
59
  DocumentUnversioned: (doc) => {
62
- const latestEntry = versioned.entries[0]!
60
+ const latestEntry = SchemaCatalog.Versioned.getLatestOrThrow(versioned)
63
61
  validateDocument(
64
62
  example.name,
65
63
  example.path,
@@ -91,10 +91,10 @@ const lintFileLayout = (
91
91
  schemaCatalog?: SchemaCatalog.Catalog,
92
92
  ): Diagnostic[] => {
93
93
  // Extract schema versions from catalog if provided
94
- const schemaVersions: string[] = schemaCatalog
94
+ const schemaVersions: Version.Version[] = schemaCatalog
95
95
  ? SchemaCatalog.fold(
96
- (versioned) => versioned.entries.map(entry => Version.encodeSync(entry.version)),
97
- (unversioned) => unversioned.schema.revisions?.map(r => r.date) ?? [],
96
+ (versioned) => SchemaCatalog.Versioned.getVersions(versioned),
97
+ (unversioned) => [], // Unversioned doesn't have Version objects, just dates
98
98
  )(schemaCatalog)
99
99
  : []
100
100
 
@@ -105,14 +105,13 @@ const lintFileLayout = (
105
105
  DocumentVersioned: (doc) => {
106
106
  // Get all versions covered by this document
107
107
  const coveredVersions = Document.Versioned.getAllVersions(doc)
108
- const coveredVersionStrings = coveredVersions.map(Version.encodeSync)
109
- const missingVersions = schemaVersions.filter(sv => !coveredVersionStrings.includes(sv))
108
+ const missingVersions = schemaVersions.filter(sv => !coveredVersions.some(cv => Version.equivalence(sv, cv)))
110
109
 
111
110
  if (missingVersions.length > 0) {
112
111
  diagnostics.push(makeDiagnosticMissingVersions({
113
112
  message: `Versioned example must provide documents for all schema versions`,
114
113
  example: { name: example.name, path: example.path },
115
- providedVersions: coveredVersionStrings,
114
+ providedVersions: coveredVersions,
116
115
  missingVersions,
117
116
  }))
118
117
  }
@@ -206,7 +205,7 @@ export const scan = (
206
205
  // Get all schema versions to map to this default document
207
206
  const schemaVersions: Version.Version[] = options.schemaCatalog
208
207
  ? SchemaCatalog.fold(
209
- (versioned) => versioned.entries.map(entry => entry.version),
208
+ (versioned) => SchemaCatalog.Versioned.getVersions(versioned),
210
209
  () => [], // Unversioned schemas don't have version-specific examples
211
210
  )(options.schemaCatalog)
212
211
  : []
@@ -238,16 +237,19 @@ export const scan = (
238
237
  // Versioned example - multiple files or versioned files
239
238
  let versionDocuments = HashMap.empty<VersionCoverage.VersionCoverage, string>()
240
239
  let defaultDocument: string | undefined
241
- const explicitVersions = new Set<string>() // Track which versions have explicit files
242
- const unknownVersions: string[] = []
240
+ let explicitVersions = HashSet.empty<Version.Version>() // Track which versions have explicit files
241
+ const unknownVersions: Version.Version[] = []
243
242
 
244
243
  // Get available schema versions if catalog is provided
245
- const schemaVersions: string[] = options.schemaCatalog
244
+ const schemaVersions: Version.Version[] = options.schemaCatalog
246
245
  ? SchemaCatalog.fold(
247
- (versioned) => versioned.entries.map(entry => Version.encodeSync(entry.version)),
246
+ (versioned) => SchemaCatalog.Versioned.getVersions(versioned),
248
247
  () => [], // Unversioned schemas don't have version-specific examples
249
248
  )(options.schemaCatalog)
250
249
  : []
250
+
251
+ // Create HashSet for O(1) lookups
252
+ const schemaVersionsSet = HashSet.fromIterable(schemaVersions)
251
253
 
252
254
  // Read content for each version
253
255
  for (const [version, filePath] of versions) {
@@ -257,35 +259,38 @@ export const scan = (
257
259
  if (version === 'default') {
258
260
  defaultDocument = fileContent
259
261
  } else if (version !== null) {
262
+ // Parse the version string
263
+ const parsedVersion = Version.decodeSync(version)
260
264
  // Check if this version exists in the schema
261
- if (options.schemaCatalog && schemaVersions.length > 0 && !schemaVersions.includes(version)) {
262
- unknownVersions.push(version)
265
+ const versionExists = HashSet.has(schemaVersionsSet, parsedVersion)
266
+ if (options.schemaCatalog && schemaVersions.length > 0 && !versionExists) {
267
+ unknownVersions.push(parsedVersion)
263
268
  // Create diagnostic for unknown version
264
269
  diagnostics.push(makeDiagnosticUnknownVersion({
265
270
  message: `Example "${name}" specifies version "${version}" which does not exist in the schema`,
266
271
  example: { name, path: basePath },
267
- version,
272
+ version: parsedVersion,
268
273
  availableVersions: schemaVersions,
269
274
  }))
270
275
  // Skip this version - don't include it in the example
271
276
  continue
272
277
  }
273
278
 
274
- const versionObj = Version.decodeSync(version)
275
- versionDocuments = HashMap.set(versionDocuments, versionObj, fileContent)
276
- explicitVersions.add(version)
279
+ // We already have parsedVersion from above
280
+ versionDocuments = HashMap.set(versionDocuments, parsedVersion, fileContent)
281
+ explicitVersions = HashSet.add(explicitVersions, parsedVersion)
277
282
  }
278
283
  }
279
284
 
280
285
  if (defaultDocument) {
281
286
  // If we have a default, determine which versions it applies to
282
- const defaultVersions = schemaVersions.filter(v => !explicitVersions.has(v))
287
+ const defaultVersions = schemaVersions.filter(v => !HashSet.has(explicitVersions, v))
283
288
 
284
289
  if (defaultVersions.length > 0) {
285
290
  // Create a version set for the default document
286
291
  const defaultVersionSet = defaultVersions.length === 1
287
- ? Version.decodeSync(defaultVersions[0]!) // Single version
288
- : HashSet.fromIterable(defaultVersions.map(_ => Version.decodeSync(_))) // Version set
292
+ ? defaultVersions[0]! // Single version
293
+ : HashSet.fromIterable(defaultVersions) // Version set
289
294
 
290
295
  versionDocuments = HashMap.set(versionDocuments, defaultVersionSet, defaultDocument)
291
296
  }
@@ -1,6 +1,5 @@
1
1
  import { Catalog } from '#lib/catalog/$'
2
2
  import { Document } from '#lib/document/$'
3
- import { Revision } from '#lib/revision/$'
4
3
  import { Schema } from '#lib/schema/$'
5
4
  import { Version } from '#lib/version/$'
6
5
  import { HashMap, HashSet, Schema as S } from 'effect'
@@ -17,23 +16,23 @@ describe('type-usage-indexer', () => {
17
16
  users: [User!]!
18
17
  product(id: ID!): Product
19
18
  }
20
-
19
+
21
20
  type User {
22
21
  id: ID!
23
22
  name: String!
24
23
  email: String!
25
24
  }
26
-
25
+
27
26
  type Product {
28
27
  id: ID!
29
28
  name: String!
30
29
  price: Float!
31
30
  }
32
-
31
+
33
32
  interface Node {
34
33
  id: ID!
35
34
  }
36
-
35
+
37
36
  union SearchResult = User | Product
38
37
  `
39
38
 
@@ -91,20 +90,26 @@ describe('type-usage-indexer', () => {
91
90
  })
92
91
 
93
92
  const catalog = Catalog.Versioned.make({
94
- entries: [
95
- Schema.Versioned.make({
96
- version: version1,
97
- definition: schemaDef,
98
- branchPoint: null,
99
- revisions: [],
100
- }),
101
- Schema.Versioned.make({
102
- version: version2,
103
- definition: schemaDef,
104
- branchPoint: null,
105
- revisions: [],
106
- }),
107
- ],
93
+ entries: HashMap.make(
94
+ [
95
+ version1,
96
+ Schema.Versioned.make({
97
+ version: version1,
98
+ definition: schemaDef,
99
+ branchPoint: null,
100
+ revisions: [],
101
+ }),
102
+ ],
103
+ [
104
+ version2,
105
+ Schema.Versioned.make({
106
+ version: version2,
107
+ definition: schemaDef,
108
+ branchPoint: null,
109
+ revisions: [],
110
+ }),
111
+ ],
112
+ ),
108
113
  })
109
114
 
110
115
  const index = createTypeUsageIndex([example], catalog)
@@ -212,20 +217,26 @@ describe('type-usage-indexer', () => {
212
217
  })
213
218
 
214
219
  const catalog = Catalog.Versioned.make({
215
- entries: [
216
- Schema.Versioned.make({
217
- version: version1,
218
- definition: schemaDef,
219
- branchPoint: null,
220
- revisions: [],
221
- }),
222
- Schema.Versioned.make({
223
- version: version2,
224
- definition: schemaDef,
225
- branchPoint: null,
226
- revisions: [],
227
- }),
228
- ],
220
+ entries: HashMap.make(
221
+ [
222
+ version1,
223
+ Schema.Versioned.make({
224
+ version: version1,
225
+ definition: schemaDef,
226
+ branchPoint: null,
227
+ revisions: [],
228
+ }),
229
+ ],
230
+ [
231
+ version2,
232
+ Schema.Versioned.make({
233
+ version: version2,
234
+ definition: schemaDef,
235
+ branchPoint: null,
236
+ revisions: [],
237
+ }),
238
+ ],
239
+ ),
229
240
  })
230
241
 
231
242
  const index = createTypeUsageIndex([example], catalog)
@@ -172,11 +172,8 @@ const getSchemaByVersion = (
172
172
  version: Version.Version,
173
173
  ): Schema.Schema | undefined => {
174
174
  if (Catalog.Versioned.is(catalog)) {
175
- // Find the schema with matching version
176
- return catalog.entries.find(entry =>
177
- Schema.Versioned.is(entry)
178
- && Version.equivalence(entry.version, version)
179
- )
175
+ // Use HashMap.get for O(1) lookup
176
+ return HashMap.get(catalog.entries, version).pipe(Option.getOrElse(() => undefined))
180
177
  }
181
178
  // For unversioned catalog, return the single schema if version matches
182
179
  if (Catalog.Unversioned.is(catalog)) {
@@ -206,12 +203,12 @@ export const createTypeUsageIndex = (
206
203
  // Process based on document type
207
204
  if (Document.Unversioned.is(example.document)) {
208
205
  // Unversioned document
209
- const schema = Catalog.getLatestSchema(schemasCatalog)
206
+ const schema = Catalog.getLatest(schemasCatalog)
210
207
  const types = extractTypesFromQuery(example.document.document, schema.definition)
211
208
 
212
209
  for (const typeName of types) {
213
210
  // For unversioned, use the latest version from the catalog
214
- const latestVersion = Catalog.getLatestVersionIdentifier(schemasCatalog) ?? Version.fromString('1.0.0')
211
+ const latestVersion = Catalog.getLatestVersion(schemasCatalog) ?? Version.fromString('1.0.0')
215
212
  index = addExampleToIndex(index, UNVERSIONED_KEY, typeName, example, latestVersion)
216
213
  }
217
214
  } else if (Document.Versioned.is(example.document)) {
@@ -3,7 +3,7 @@ import { Catalog } from '#lib/catalog/$'
3
3
  import { Grafaid } from '#lib/grafaid'
4
4
  import { MemoryFilesystem } from '#lib/memory-filesystem/$'
5
5
  import * as NodeFileSystem from '@effect/platform-node/NodeFileSystem'
6
- import { Effect } from 'effect'
6
+ import { Effect, HashMap } from 'effect'
7
7
  import { expect } from 'vitest'
8
8
  import { Test } from '../../../tests/unit/helpers/test.js'
9
9
  import { Schema } from './$.js'
@@ -421,7 +421,7 @@ testWithFileSystem<BaseTestCase & {
421
421
  expect(result!._tag).toBe('CatalogVersioned')
422
422
  if (expected.versionCount !== undefined) {
423
423
  const versioned = result as Catalog.Versioned.Versioned
424
- expect(versioned.entries.length).toBe(expected.versionCount)
424
+ expect(HashMap.size(versioned.entries)).toBe(expected.versionCount)
425
425
  }
426
426
  } else {
427
427
  expect(result!._tag).toBe('CatalogUnversioned')
@@ -10,7 +10,7 @@ import { debugPolen } from '#singletons/debug'
10
10
  import { PlatformError } from '@effect/platform/Error'
11
11
  import { FileSystem } from '@effect/platform/FileSystem'
12
12
  import { Arr, Path } from '@wollybeard/kit'
13
- import { Effect } from 'effect'
13
+ import { Effect, HashMap } from 'effect'
14
14
  import type { GraphQLSchema } from 'graphql'
15
15
 
16
16
  const debug = debugPolen.sub(`schema:data-source-versioned-schema-directory`)
@@ -393,9 +393,14 @@ export const readOrThrow = (
393
393
  // Reverse to have newest first
394
394
  catalogEntries.reverse()
395
395
 
396
+ // Convert array to HashMap with version as key
397
+ const entriesMap = HashMap.fromIterable(
398
+ catalogEntries.map(entry => [entry.version, entry] as const),
399
+ )
400
+
396
401
  debug(`computed ${catalogEntries.length} entries`)
397
402
  return Catalog.Versioned.make({
398
- entries: catalogEntries,
403
+ entries: entriesMap,
399
404
  })
400
405
  })
401
406
 
@@ -142,7 +142,7 @@ export const loadOrNull = (
142
142
  const catalog = loadedSchema.data as Catalog.Catalog
143
143
  Catalog.fold(
144
144
  (versioned) => {
145
- for (const schema of versioned.entries) {
145
+ for (const schema of Catalog.Versioned.getAll(versioned)) {
146
146
  Augmentations.apply(schema.definition, augmentations)
147
147
  }
148
148
  },
@@ -133,7 +133,7 @@ export const heroImage = Command.make(
133
133
  const schemaResult = yield* Api.Schema.loadOrNull(config)
134
134
  if (schemaResult?.data) {
135
135
  try {
136
- const latestSchema = Catalog.getLatestSchema(schemaResult.data)
136
+ const latestSchema = Catalog.getLatest(schemaResult.data)
137
137
  if (latestSchema?.definition) {
138
138
  schema = latestSchema.definition
139
139
  const context = AiImageGeneration.analyzeSchema(schema)
@@ -1,7 +1,7 @@
1
1
  import { S } from '#lib/kit-temp/effect'
2
2
  import { Schema } from '#lib/schema/$'
3
3
  import { Version } from '#lib/version/$'
4
- import { Match } from 'effect'
4
+ import { HashMap, Match } from 'effect'
5
5
  import * as Unversioned from './unversioned.js'
6
6
  import * as Versioned from './versioned.js'
7
7
 
@@ -62,7 +62,7 @@ export const equivalence = S.equivalence(Catalog)
62
62
  */
63
63
  export const getVersionCount = (catalog: Catalog): number =>
64
64
  fold(
65
- (versioned) => versioned.entries.length,
65
+ (versioned) => HashMap.size(versioned.entries),
66
66
  (_unversioned) => 1, // Unversioned catalog is effectively one version
67
67
  )(catalog)
68
68
 
@@ -79,28 +79,22 @@ export const getSchemaVersionString = (schema: Schema.Schema): string => {
79
79
  * Get the version string from a schema.
80
80
  * Returns the stringified version for versioned schemas, or '__UNVERSIONED__' for unversioned schemas.
81
81
  */
82
- export const getLatestSchema = (catalog: Catalog): Schema.Schema =>
82
+ export const getLatest = (catalog: Catalog): Schema.Schema =>
83
83
  Match.value(catalog).pipe(Match.tagsExhaustive({
84
- CatalogVersioned: (versioned) => {
85
- // Entries are sorted newest first, so get the first entry
86
- const latestEntry = versioned.entries[0]!
87
- return latestEntry
88
- },
89
- CatalogUnversioned: (unversioned) => {
90
- return unversioned.schema
91
- },
84
+ CatalogVersioned: Versioned.getLatestOrThrow,
85
+ CatalogUnversioned: (unversioned) => unversioned.schema,
92
86
  }))
93
87
 
94
88
  /**
95
89
  * Get the latest version identifier from a catalog.
96
90
  * Returns the version for versioned catalogs, or null for unversioned catalogs.
97
91
  */
98
- export const getLatestVersionIdentifier = (catalog?: Catalog): Version.Version | null => {
92
+ export const getLatestVersion = (catalog?: Catalog): Version.Version | null => {
99
93
  if (!catalog) return null
100
94
  return Match.value(catalog).pipe(
101
95
  Match.tagsExhaustive({
102
96
  CatalogUnversioned: () => null,
103
- CatalogVersioned: (cat) => cat.entries[0]?.version ?? null,
97
+ CatalogVersioned: (cat) => Versioned.getVersions(cat)[0] ?? null,
104
98
  }),
105
99
  )
106
100
  }
@@ -1,12 +1,17 @@
1
1
  import { S } from '#lib/kit-temp/effect'
2
+ import { Array, HashMap, Iterable, Order, pipe } from 'effect'
2
3
  import { Schema } from '../schema/$.js'
4
+ import { Version } from '../version/$.js'
3
5
 
4
6
  // ============================================================================
5
7
  // Schema
6
8
  // ============================================================================
7
9
 
8
10
  export const Versioned = S.TaggedStruct('CatalogVersioned', {
9
- entries: S.Array(Schema.Versioned.Versioned),
11
+ entries: S.HashMap({
12
+ key: Version.Version,
13
+ value: Schema.Versioned.Versioned,
14
+ }),
10
15
  }).annotations({
11
16
  identifier: 'CatalogVersioned',
12
17
  title: 'Versioned Catalog',
@@ -47,16 +52,40 @@ export const equivalence = S.equivalence(Versioned)
47
52
 
48
53
  /**
49
54
  * Get the latest schema definition from a versioned catalog.
50
- * The latest version is the first entry in the catalog (entries are ordered newest to oldest).
55
+ * The latest version is determined by Version.max comparison.
51
56
  *
52
57
  * @param catalog - The versioned catalog
53
58
  * @returns The GraphQL schema definition of the latest version
54
59
  * @throws {Error} If the catalog has no entries
55
60
  */
56
- export const getLatest = (catalog: Versioned): Schema.Schema => {
57
- const latestEntry = catalog.entries[0]
58
- if (!latestEntry) {
61
+ export const getLatestOrThrow = (catalog: Versioned): Schema.Versioned.Versioned => {
62
+ const schema = getAll(catalog)[0]
63
+ if (!schema) {
59
64
  throw new Error('Versioned catalog has no entries - cannot get latest schema')
60
65
  }
61
- return latestEntry
66
+ return schema
67
+ }
68
+
69
+ /**
70
+ * Get all schemas sorted by version (newest first)
71
+ */
72
+ export const getAll = (catalog: Versioned): Schema.Versioned.Versioned[] => {
73
+ return pipe(
74
+ catalog.entries,
75
+ HashMap.values,
76
+ Array.fromIterable,
77
+ // Put newest versions first in array
78
+ Array.sort(Order.reverse(Schema.Versioned.order)),
79
+ )
80
+ }
81
+
82
+ /**
83
+ * Get all versions sorted (newest first)
84
+ */
85
+ export const getVersions = (catalog: Versioned): Version.Version[] => {
86
+ return pipe(
87
+ catalog,
88
+ getAll,
89
+ Array.map(_ => _.version),
90
+ )
62
91
  }
@@ -1,6 +1,7 @@
1
1
  import { Catalog } from '#lib/catalog/$'
2
2
  import { DateOnly } from '#lib/date-only/$'
3
3
  import { Version } from '#lib/version/$'
4
+ import { HashMap } from 'effect'
4
5
  import { buildSchema, type GraphQLSchema } from 'graphql'
5
6
  import { describe, expect, test } from 'vitest'
6
7
  import { CatalogStatistics } from './$.js'
@@ -91,14 +92,20 @@ describe('analyzeCatalog', () => {
91
92
  name: 'versioned catalog',
92
93
  catalog: () =>
93
94
  Catalog.Versioned.make({
94
- entries: [
95
- makeVersionedEntry(1, buildSchema('type Query { hello: String }'), ['2024-01-01', '2024-01-15']),
96
- makeVersionedEntry(
97
- 2,
98
- buildSchema('type Query { hello: String, world: String } type User { id: ID!, name: String }'),
99
- ['2024-02-01'],
100
- ),
101
- ],
95
+ entries: HashMap.make(
96
+ [
97
+ Version.fromInteger(1),
98
+ makeVersionedEntry(1, buildSchema('type Query { hello: String }'), ['2024-01-01', '2024-01-15']),
99
+ ],
100
+ [
101
+ Version.fromInteger(2),
102
+ makeVersionedEntry(
103
+ 2,
104
+ buildSchema('type Query { hello: String, world: String } type User { id: ID!, name: String }'),
105
+ ['2024-02-01'],
106
+ ),
107
+ ],
108
+ ),
102
109
  }),
103
110
  expectedVersions: 2,
104
111
  expectedCurrentVersion: '2',
@@ -132,10 +139,13 @@ describe('analyzeCatalog', () => {
132
139
 
133
140
  test('calculates stability metrics', () => {
134
141
  const catalog = Catalog.Versioned.make({
135
- entries: [
136
- makeVersionedEntry(1, buildSchema('type Query { test: String }'), ['2024-01-01', '2024-01-05']),
137
- makeVersionedEntry(2, buildSchema('type Query { test: String }'), ['2024-01-10']),
138
- ],
142
+ entries: HashMap.make(
143
+ [
144
+ Version.fromInteger(1),
145
+ makeVersionedEntry(1, buildSchema('type Query { test: String }'), ['2024-01-01', '2024-01-05']),
146
+ ],
147
+ [Version.fromInteger(2), makeVersionedEntry(2, buildSchema('type Query { test: String }'), ['2024-01-10'])],
148
+ ),
139
149
  })
140
150
 
141
151
  const report = CatalogStatistics.analyzeCatalog(catalog)
@@ -16,7 +16,7 @@ export const analyzeCatalog = (catalog: Catalog.Catalog, options: AnalyzeOptions
16
16
  const processResult = Catalog.fold(
17
17
  // Versioned catalog
18
18
  (versioned) => {
19
- for (const entry of versioned.entries) {
19
+ for (const entry of Catalog.Versioned.getAll(versioned)) {
20
20
  const versionId = Version.encodeSync(entry.version)
21
21
  // Analyze the schema definition for this version
22
22
  // Use the first revision date if available
@@ -55,8 +55,8 @@ export const analyzeCatalog = (catalog: Catalog.Catalog, options: AnalyzeOptions
55
55
  // Calculate stability metrics
56
56
  const stability = calculateStabilityMetrics(versions, revisionDates)
57
57
 
58
- // Get current version (last in array)
59
- const current = versions[versions.length - 1]
58
+ // Get current version (first in array since sorted newest first)
59
+ const current = versions[0]
60
60
 
61
61
  return {
62
62
  stability,
@@ -119,7 +119,7 @@ export const create = (catalog: Catalog.Catalog): Lifecycles => {
119
119
  Catalog.fold(
120
120
  // Versioned catalog - process each versioned schema
121
121
  (versioned) => {
122
- for (const schema of versioned.entries) {
122
+ for (const schema of Catalog.Versioned.getAll(versioned)) {
123
123
  processSchema(schema)
124
124
  }
125
125
  },
@@ -4,6 +4,7 @@ import { DateOnly } from '#lib/date-only/$'
4
4
  import { Revision } from '#lib/revision/$'
5
5
  import { Schema } from '#lib/schema/$'
6
6
  import { Version } from '#lib/version/$'
7
+ import { HashMap, Option } from 'effect'
7
8
  const CRITICALITY_LEVELS = ['BREAKING', 'DANGEROUS', 'NON_BREAKING'] as const
8
9
  import type { CriticalityLevel } from '@graphql-inspector/core'
9
10
  import { Box, Heading } from '@radix-ui/themes'
@@ -35,8 +36,7 @@ export const Changelog: React.FC<{ catalog: Catalog.Catalog }> = ({ catalog }) =
35
36
  useEffect(() => {
36
37
  if (urlVersion) return
37
38
  if (Catalog.Unversioned.is(catalog)) return
38
- const latestSchema = catalog.entries[0]
39
- if (!latestSchema) return
39
+ const latestSchema = Catalog.Versioned.getLatestOrThrow(catalog)
40
40
  const latestVersion = Version.encodeSync(latestSchema.version)
41
41
  navigate(`/changelog/version/${latestVersion}`, { replace: true })
42
42
  }, [catalog, urlVersion, navigate])
@@ -51,10 +51,14 @@ export const Changelog: React.FC<{ catalog: Catalog.Catalog }> = ({ catalog }) =
51
51
  } else {
52
52
  // For versioned catalogs, always show specific version (never all)
53
53
  if (urlVersion) {
54
- const entry = catalog.entries.find(e => Version.encodeSync(e.version) === urlVersion)
55
- return entry
56
- ? { revisions: entry.revisions, schema: entry }
57
- : { revisions: [], schema: null }
54
+ const entryOption = Option.map(
55
+ HashMap.findFirst(catalog.entries, (_, key) => Version.encodeSync(key) === urlVersion),
56
+ ([, value]) => value,
57
+ )
58
+ return Option.match(entryOption, {
59
+ onNone: () => ({ revisions: [], schema: null }),
60
+ onSome: (entry) => ({ revisions: entry.revisions, schema: entry }),
61
+ })
58
62
  }
59
63
  // This shouldn't happen due to redirect above, but return empty as fallback
60
64
  return { revisions: [], schema: null }
@@ -5,7 +5,7 @@ import type { Schema } from '#lib/schema/$'
5
5
  import { VersionCoverage } from '#lib/version-selection/$'
6
6
  import { VersionCoverageSet } from '#lib/version-selection/version-selection'
7
7
  import { Version } from '#lib/version/$'
8
- import { Array, HashMap, Match } from 'effect'
8
+ import { Array, HashMap, Match, Option } from 'effect'
9
9
  import type { GraphQLSchema } from 'graphql'
10
10
  import * as React from 'react'
11
11
  import { useHighlighted } from '../hooks/use-highlighted.js'
@@ -47,7 +47,7 @@ export const GraphQLDocument: React.FC<GraphQLDocumentProps> = ({
47
47
  /// ━ VERSION MANAGEMENT
48
48
  const isControlled = controlledVersionCoverage !== undefined
49
49
  const [internalVersionCoverage, setInternalVersionCoverage] = React.useState<VersionCoverage.VersionCoverage | null>(
50
- Catalog.getLatestVersionIdentifier(schemaCatalog),
50
+ Catalog.getLatestVersion(schemaCatalog),
51
51
  )
52
52
  const selectedVersionCoverage = isControlled ? controlledVersionCoverage : internalVersionCoverage
53
53
  const internalOnVersionChange = (version: VersionCoverage.VersionCoverage) => {
@@ -110,7 +110,7 @@ const resolveSelectedVerCov = (
110
110
  schema: Match.value(schemaCatalog).pipe(
111
111
  Match.tagsExhaustive({
112
112
  CatalogUnversioned: (catalog) => catalog.schema,
113
- CatalogVersioned: (catalog) => Catalog.Versioned.getLatest(catalog),
113
+ CatalogVersioned: (catalog) => Catalog.Versioned.getLatestOrThrow(catalog),
114
114
  }),
115
115
  ),
116
116
  }
@@ -134,10 +134,11 @@ const resolveSelectedVerCov = (
134
134
  if (Catalog.Unversioned.is(schemaCatalog)) {
135
135
  throw new Error('Cannot use a set of versions with an unversioned catalog')
136
136
  }
137
- const schema = schemaCatalog.entries.find(e => Version.equivalence(e.version, version))
138
- if (!schema) {
137
+ const schemaOption = HashMap.get(schemaCatalog.entries, version)
138
+ if (Option.isNone(schemaOption)) {
139
139
  throw new Error(`Version ${Version.encodeSync(version)} not found in catalog`)
140
140
  }
141
+ const schema = Option.getOrThrow(schemaOption)
141
142
 
142
143
  return { content, schema: schema }
143
144
  }),