polen 0.11.0-next.7 → 0.11.0-next.8
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/build/api/config/configurator.d.ts +35 -4
- package/build/api/config/configurator.d.ts.map +1 -1
- package/build/api/config/configurator.js.map +1 -1
- package/build/api/schema/data-sources/data-sources.d.ts +1 -0
- package/build/api/schema/data-sources/data-sources.d.ts.map +1 -1
- package/build/api/schema/data-sources/data-sources.js +1 -0
- package/build/api/schema/data-sources/data-sources.js.map +1 -1
- package/build/api/schema/data-sources/introspection/introspection.d.ts +83 -0
- package/build/api/schema/data-sources/introspection/introspection.d.ts.map +1 -0
- package/build/api/schema/data-sources/introspection/introspection.js +110 -0
- package/build/api/schema/data-sources/introspection/introspection.js.map +1 -0
- package/build/api/schema/read.d.ts +83 -9
- package/build/api/schema/read.d.ts.map +1 -1
- package/build/api/schema/read.js +15 -6
- package/build/api/schema/read.js.map +1 -1
- package/build/api/schema-source/schema-source.d.ts +1 -1
- package/build/api/schema-source/schema-source.d.ts.map +1 -1
- package/build/api/schema-source/schema-source.js.map +1 -1
- package/build/api/vite/plugins/core.js +8 -8
- package/build/api/vite/plugins/core.js.map +1 -1
- package/build/api/vite/plugins/schema-assets.d.ts.map +1 -1
- package/build/api/vite/plugins/schema-assets.js +52 -11
- package/build/api/vite/plugins/schema-assets.js.map +1 -1
- package/build/cli/commands/open.js +1 -1
- package/build/cli/commands/open.js.map +1 -1
- package/build/lib/grafaid/schema/schema.d.ts +1 -1
- package/build/lib/grafaid/schema/schema.d.ts.map +1 -1
- package/build/lib/grafaid/schema/schema.js +1 -1
- package/build/lib/grafaid/schema/schema.js.map +1 -1
- package/build/lib/kit-temp.js +2 -2
- package/build/lib/kit-temp.js.map +1 -1
- package/build/template/sources/schema-source.d.ts +1 -1
- package/package.json +1 -1
- package/src/api/config/configurator.ts +35 -4
- package/src/api/schema/data-sources/data-sources.ts +1 -0
- package/src/api/schema/data-sources/introspection/introspection.ts +213 -0
- package/src/api/schema/read.ts +107 -16
- package/src/api/schema-source/schema-source.ts +3 -3
- package/src/api/vite/plugins/core.ts +8 -8
- package/src/api/vite/plugins/schema-assets.ts +59 -12
- package/src/cli/commands/open.ts +1 -1
- package/src/lib/grafaid/schema/schema.ts +1 -0
- package/src/lib/kit-temp.ts +2 -2
- package/src/lib/mask/$.test.ts +3 -3
@@ -0,0 +1,213 @@
|
|
1
|
+
import { Grafaid } from '#lib/grafaid/index'
|
2
|
+
import { GraphqlChange } from '#lib/graphql-change/index'
|
3
|
+
import type { GraphqlChangeset } from '#lib/graphql-changeset/index'
|
4
|
+
import { GraphqlSchemaLoader } from '#lib/graphql-schema-loader/index'
|
5
|
+
import { Fs, Json, Path } from '@wollybeard/kit'
|
6
|
+
import type { NonEmptyChangeSets, SchemaReadResult } from '../../schema.js'
|
7
|
+
|
8
|
+
/**
|
9
|
+
* Configuration for loading schema via GraphQL introspection.
|
10
|
+
*
|
11
|
+
* Polen supports two introspection features:
|
12
|
+
* 1. **File Convention**: Automatically detects `schema.introspection.json` if present
|
13
|
+
* 2. **Automatic Introspection**: Fetches from your endpoint and creates the file
|
14
|
+
*
|
15
|
+
* When configured, Polen will:
|
16
|
+
* - Execute the standard GraphQL introspection query against your endpoint
|
17
|
+
* - Save the result to `schema.introspection.json` in your project root
|
18
|
+
* - Use this cached file for subsequent builds (no network requests)
|
19
|
+
*
|
20
|
+
* The saved file contains a standard GraphQL introspection query result as defined
|
21
|
+
* in the GraphQL specification, making it compatible with any tool that works with
|
22
|
+
* introspection data (GraphQL Codegen, Apollo CLI, etc.).
|
23
|
+
*
|
24
|
+
* To refresh the schema, delete `schema.introspection.json` and rebuild.
|
25
|
+
*
|
26
|
+
* **Technical details**:
|
27
|
+
* - Uses Graffle's introspection extension
|
28
|
+
* - Performs the full introspection query (all types, fields, descriptions, etc.)
|
29
|
+
* - No customization of the query is currently supported
|
30
|
+
*
|
31
|
+
* @see https://spec.graphql.org/draft/#sec-Introspection - GraphQL Introspection spec
|
32
|
+
* @see https://github.com/graphql/graphql-js/blob/main/src/utilities/getIntrospectionQuery.ts - Reference implementation
|
33
|
+
*
|
34
|
+
* @example
|
35
|
+
* ```ts
|
36
|
+
* // Basic introspection
|
37
|
+
* introspection: {
|
38
|
+
* url: 'https://api.example.com/graphql'
|
39
|
+
* }
|
40
|
+
*
|
41
|
+
* // With authentication
|
42
|
+
* introspection: {
|
43
|
+
* url: 'https://api.example.com/graphql',
|
44
|
+
* headers: {
|
45
|
+
* 'Authorization': `Bearer ${process.env.API_TOKEN}`
|
46
|
+
* }
|
47
|
+
* }
|
48
|
+
* ```
|
49
|
+
*/
|
50
|
+
export interface ConfigInput {
|
51
|
+
/**
|
52
|
+
* The GraphQL endpoint URL to introspect.
|
53
|
+
*
|
54
|
+
* Must be a valid GraphQL endpoint that supports introspection queries.
|
55
|
+
*
|
56
|
+
* @example 'https://api.example.com/graphql'
|
57
|
+
*/
|
58
|
+
url?: string
|
59
|
+
/**
|
60
|
+
* Optional headers to include in the introspection request.
|
61
|
+
*
|
62
|
+
* Use this for authentication, API keys, or any custom headers
|
63
|
+
* required by your GraphQL endpoint.
|
64
|
+
*
|
65
|
+
* @example
|
66
|
+
* ```ts
|
67
|
+
* headers: {
|
68
|
+
* 'Authorization': `Bearer ${process.env.API_TOKEN}`,
|
69
|
+
* 'X-API-Key': process.env.API_KEY
|
70
|
+
* }
|
71
|
+
* ```
|
72
|
+
*/
|
73
|
+
headers?: Record<string, string>
|
74
|
+
projectRoot?: string
|
75
|
+
}
|
76
|
+
|
77
|
+
export interface Config {
|
78
|
+
url: string
|
79
|
+
headers?: Record<string, string>
|
80
|
+
projectRoot: string
|
81
|
+
}
|
82
|
+
|
83
|
+
export const normalizeConfig = (configInput: ConfigInput): Config => {
|
84
|
+
if (!configInput.url) {
|
85
|
+
throw new Error(`Introspection data source requires a URL`)
|
86
|
+
}
|
87
|
+
|
88
|
+
if (!configInput.projectRoot) {
|
89
|
+
throw new Error(`Introspection data source requires a projectRoot`)
|
90
|
+
}
|
91
|
+
|
92
|
+
const config: Config = {
|
93
|
+
url: configInput.url,
|
94
|
+
headers: configInput.headers,
|
95
|
+
projectRoot: configInput.projectRoot,
|
96
|
+
}
|
97
|
+
|
98
|
+
return config
|
99
|
+
}
|
100
|
+
|
101
|
+
const INTROSPECTION_FILE_NAME = `schema.introspection.json`
|
102
|
+
|
103
|
+
const getIntrospectionFilePath = (projectRoot: string) => {
|
104
|
+
return Path.join(projectRoot, INTROSPECTION_FILE_NAME)
|
105
|
+
}
|
106
|
+
|
107
|
+
export const readOrThrow = async (
|
108
|
+
configInput: ConfigInput,
|
109
|
+
): Promise<SchemaReadResult> => {
|
110
|
+
const config = normalizeConfig(configInput)
|
111
|
+
const introspectionFilePath = getIntrospectionFilePath(config.projectRoot)
|
112
|
+
|
113
|
+
// Check if introspection file exists
|
114
|
+
const introspectionFileContent = await Fs.read(introspectionFilePath)
|
115
|
+
let schema: Grafaid.Schema.Schema
|
116
|
+
|
117
|
+
if (introspectionFileContent) {
|
118
|
+
// Load from existing file - no reCreate capability
|
119
|
+
try {
|
120
|
+
const introspectionData = Json.codec.decode(introspectionFileContent)
|
121
|
+
|
122
|
+
// Validate introspection data structure before passing to fromIntrospectionQuery
|
123
|
+
if (!introspectionData || typeof introspectionData !== 'object') {
|
124
|
+
throw new Error('Introspection data must be a valid JSON object')
|
125
|
+
}
|
126
|
+
|
127
|
+
// Allow fromIntrospectionQuery to handle validation of the introspection format
|
128
|
+
// It will provide more specific GraphQL-related error messages
|
129
|
+
if (!('data' in introspectionData)) {
|
130
|
+
throw new Error(
|
131
|
+
'Introspection data missing required "data" property (expected GraphQL introspection result format)',
|
132
|
+
)
|
133
|
+
}
|
134
|
+
|
135
|
+
schema = Grafaid.Schema.fromIntrospectionQuery(introspectionData as any)
|
136
|
+
} catch (error) {
|
137
|
+
if (error instanceof SyntaxError) {
|
138
|
+
throw new Error(`Invalid JSON in ${introspectionFilePath}: ${error.message}`)
|
139
|
+
} else {
|
140
|
+
throw new Error(
|
141
|
+
`Invalid introspection data in ${introspectionFilePath}: ${
|
142
|
+
error instanceof Error ? error.message : String(error)
|
143
|
+
}. Delete this file to fetch fresh introspection data.`,
|
144
|
+
)
|
145
|
+
}
|
146
|
+
}
|
147
|
+
|
148
|
+
const schemaData = await createSingleSchemaChangeset(schema)
|
149
|
+
return {
|
150
|
+
data: schemaData,
|
151
|
+
source: { type: 'introspectionFile' },
|
152
|
+
}
|
153
|
+
} else {
|
154
|
+
// Fetch via introspection - can reCreate
|
155
|
+
const introspectionResult = await GraphqlSchemaLoader.load({
|
156
|
+
type: `introspect`,
|
157
|
+
url: config.url,
|
158
|
+
headers: config.headers,
|
159
|
+
})
|
160
|
+
|
161
|
+
schema = introspectionResult
|
162
|
+
|
163
|
+
// Get the raw introspection result for saving
|
164
|
+
const __schema = Grafaid.Schema.toIntrospectionQuery(schema)
|
165
|
+
|
166
|
+
// Wrap in GraphQL response format for consistency with API responses
|
167
|
+
const introspectionData = { data: { __schema } }
|
168
|
+
|
169
|
+
// Write to file
|
170
|
+
await Fs.write({
|
171
|
+
path: introspectionFilePath,
|
172
|
+
content: Json.codec.encode(introspectionData as any),
|
173
|
+
})
|
174
|
+
|
175
|
+
const schemaData = await createSingleSchemaChangeset(schema)
|
176
|
+
return {
|
177
|
+
data: schemaData,
|
178
|
+
source: {
|
179
|
+
type: 'introspectionAuto',
|
180
|
+
reCreate: async () => {
|
181
|
+
// Re-fetch using captured config - capture closure
|
182
|
+
const result = await readOrThrow(configInput)
|
183
|
+
return result.data
|
184
|
+
},
|
185
|
+
},
|
186
|
+
}
|
187
|
+
}
|
188
|
+
}
|
189
|
+
|
190
|
+
/**
|
191
|
+
* Create a single changeset from a schema object.
|
192
|
+
* This is the core logic for handling single (unversioned) schemas from introspection.
|
193
|
+
*/
|
194
|
+
export const createSingleSchemaChangeset = async (schema: Grafaid.Schema.Schema): Promise<NonEmptyChangeSets> => {
|
195
|
+
const date = new Date() // Generate date here for unversioned schema
|
196
|
+
const after = schema
|
197
|
+
const before = Grafaid.Schema.empty
|
198
|
+
const changes = await GraphqlChange.calcChangeset({
|
199
|
+
before,
|
200
|
+
after,
|
201
|
+
})
|
202
|
+
|
203
|
+
const changeset: GraphqlChangeset.ChangeSet = {
|
204
|
+
date,
|
205
|
+
after,
|
206
|
+
before,
|
207
|
+
changes,
|
208
|
+
}
|
209
|
+
|
210
|
+
const result: NonEmptyChangeSets = [changeset]
|
211
|
+
|
212
|
+
return result
|
213
|
+
}
|
package/src/api/schema/read.ts
CHANGED
@@ -1,38 +1,104 @@
|
|
1
|
-
import { Arr } from '@wollybeard/kit'
|
1
|
+
import { Arr, Fs, Path } from '@wollybeard/kit'
|
2
2
|
import * as DataSources from './data-sources/data-sources.js'
|
3
3
|
import type { NonEmptyChangeSets } from './schema.js'
|
4
4
|
|
5
|
-
export type DataSourceType =
|
5
|
+
export type DataSourceType =
|
6
|
+
| `file`
|
7
|
+
| `directory`
|
8
|
+
| `memory`
|
9
|
+
| `data`
|
10
|
+
| `introspection`
|
11
|
+
| `introspectionFile`
|
12
|
+
| `introspectionAuto`
|
13
|
+
|
14
|
+
/**
|
15
|
+
* Result of schema reading with provenance tracking for file watching and debugging.
|
16
|
+
*/
|
17
|
+
export interface SchemaReadResult {
|
18
|
+
data: NonEmptyChangeSets | null
|
19
|
+
source: {
|
20
|
+
type: DataSourceType
|
21
|
+
/**
|
22
|
+
* Recreate the schema data and file after it has been deleted.
|
23
|
+
*
|
24
|
+
* This function re-fetches data from the original source and recreates
|
25
|
+
* the schema file on disk. Only called by file watchers after deletion.
|
26
|
+
*
|
27
|
+
* @returns Promise resolving to the recreated schema data, or null if recreation fails
|
28
|
+
*/
|
29
|
+
reCreate?: () => Promise<NonEmptyChangeSets | null>
|
30
|
+
}
|
31
|
+
}
|
6
32
|
|
7
33
|
/**
|
8
34
|
* Schema configuration for Polen.
|
35
|
+
*
|
36
|
+
* Polen supports multiple ways to load your GraphQL schema, from simple files
|
37
|
+
* to dynamic introspection. Configure which sources to use and in what order.
|
38
|
+
*
|
39
|
+
* @example
|
40
|
+
* ```ts
|
41
|
+
* // Load from a file (default)
|
42
|
+
* schema: {} // Looks for schema.graphql
|
43
|
+
*
|
44
|
+
* // Load via introspection
|
45
|
+
* schema: {
|
46
|
+
* dataSources: {
|
47
|
+
* introspection: {
|
48
|
+
* url: 'https://api.example.com/graphql',
|
49
|
+
* headers: { 'Authorization': 'Bearer token' }
|
50
|
+
* }
|
51
|
+
* }
|
52
|
+
* }
|
53
|
+
*
|
54
|
+
* // Try multiple sources in order
|
55
|
+
* schema: {
|
56
|
+
* useDataSources: ['introspection', 'file'],
|
57
|
+
* dataSources: {
|
58
|
+
* introspection: { url: 'https://api.example.com/graphql' },
|
59
|
+
* file: { path: './fallback-schema.graphql' }
|
60
|
+
* }
|
61
|
+
* }
|
62
|
+
* ```
|
9
63
|
*/
|
10
64
|
export interface Config {
|
11
65
|
/**
|
12
66
|
* Whether to enable schema loading.
|
13
67
|
*
|
14
|
-
* Set to `false` to disable schema features entirely.
|
68
|
+
* Set to `false` to disable schema features entirely. This removes
|
69
|
+
* the Reference and Changelog pages from your portal.
|
15
70
|
*
|
16
|
-
* @default
|
71
|
+
* @default true
|
72
|
+
*
|
73
|
+
* @example
|
74
|
+
* ```ts
|
75
|
+
* // Disable schema features
|
76
|
+
* schema: { enabled: false }
|
77
|
+
* ```
|
17
78
|
*/
|
18
79
|
enabled?: boolean
|
19
80
|
/**
|
20
81
|
* Which data sources to use for loading schemas.
|
21
82
|
*
|
22
|
-
* - `file` - Load from a single SDL file
|
23
|
-
* - `directory` - Load multiple SDL files from a directory
|
83
|
+
* - `file` - Load from a single SDL file (default: `./schema.graphql`)
|
84
|
+
* - `directory` - Load multiple SDL files from a directory (default: `./schema/`)
|
24
85
|
* - `memory` - Use schemas defined in configuration
|
25
86
|
* - `data` - Use a pre-built schema object
|
87
|
+
* - `introspection` - Load schema via GraphQL introspection
|
26
88
|
*
|
27
|
-
* If not specified, Polen
|
89
|
+
* If not specified, Polen tries all sources in this order:
|
90
|
+
* 1. `data` 2. `directory` 3. `file` 4. `memory` 5. `introspection`
|
28
91
|
*
|
29
92
|
* @example
|
30
93
|
* ```ts
|
31
94
|
* // Use only file source
|
32
95
|
* useDataSources: 'file'
|
33
96
|
*
|
34
|
-
* // Try multiple sources
|
35
|
-
* useDataSources: ['
|
97
|
+
* // Try multiple sources in custom order
|
98
|
+
* useDataSources: ['introspection', 'file']
|
99
|
+
*
|
100
|
+
* // Default behavior (try all sources)
|
101
|
+
* // useDataSources: undefined
|
36
102
|
* ```
|
37
103
|
*/
|
38
104
|
useDataSources?: Arr.Maybe<DataSourceType>
|
@@ -56,22 +122,38 @@ export interface Config {
|
|
56
122
|
* Pre-built schema object to use directly.
|
57
123
|
*/
|
58
124
|
data?: NonEmptyChangeSets
|
125
|
+
/**
|
126
|
+
* Configuration for loading schema via GraphQL introspection.
|
127
|
+
*
|
128
|
+
* Introspection fetches your schema directly from a running GraphQL endpoint.
|
129
|
+
* The schema is saved to `schema.introspection.json` in your project root.
|
130
|
+
* Delete this file to force a fresh introspection on the next build.
|
131
|
+
*
|
132
|
+
* @example
|
133
|
+
* ```ts
|
134
|
+
* introspection: {
|
135
|
+
* url: 'https://api.example.com/graphql',
|
136
|
+
* headers: { 'Authorization': 'Bearer token' }
|
137
|
+
* }
|
138
|
+
* ```
|
139
|
+
*/
|
140
|
+
introspection?: DataSources.Introspection.ConfigInput
|
59
141
|
}
|
60
142
|
projectRoot: string
|
61
143
|
}
|
62
144
|
|
63
145
|
export const readOrThrow = async (
|
64
146
|
config: Config,
|
65
|
-
): Promise<
|
147
|
+
): Promise<SchemaReadResult> => {
|
66
148
|
if (config.enabled === false) {
|
67
|
-
return null
|
149
|
+
return { data: null, source: { type: 'data' } }
|
68
150
|
}
|
69
151
|
|
70
152
|
const useDataSources = config.useDataSources ? Arr.sure(config.useDataSources) : null
|
71
153
|
const usingDataSource = (dataSource: DataSourceType) => useDataSources === null || useDataSources.includes(dataSource)
|
72
154
|
|
73
155
|
if (usingDataSource(`data`) && config.dataSources?.data) {
|
74
|
-
return config.dataSources.data
|
156
|
+
return { data: config.dataSources.data, source: { type: 'data' } }
|
75
157
|
}
|
76
158
|
|
77
159
|
if (usingDataSource(`directory`)) {
|
@@ -80,7 +162,7 @@ export const readOrThrow = async (
|
|
80
162
|
...config.dataSources?.directory,
|
81
163
|
}
|
82
164
|
const result = await DataSources.SchemaDirectory.readOrThrow(directoryConfig)
|
83
|
-
if (result) return result
|
165
|
+
if (result) return { data: result, source: { type: 'directory' } }
|
84
166
|
}
|
85
167
|
|
86
168
|
if (usingDataSource(`file`)) {
|
@@ -89,7 +171,7 @@ export const readOrThrow = async (
|
|
89
171
|
...config.dataSources?.file,
|
90
172
|
}
|
91
173
|
const result = await DataSources.SchemaFile.readOrThrow(fileConfig)
|
92
|
-
if (result) return result
|
174
|
+
if (result) return { data: result, source: { type: 'file' } }
|
93
175
|
}
|
94
176
|
|
95
177
|
if (usingDataSource(`memory`) && config.dataSources?.memory) {
|
@@ -98,8 +180,17 @@ export const readOrThrow = async (
|
|
98
180
|
...config.dataSources.memory,
|
99
181
|
}
|
100
182
|
const result = await DataSources.Memory.read(memoryConfig)
|
101
|
-
if (result) return result
|
183
|
+
if (result) return { data: result, source: { type: 'memory' } }
|
184
|
+
}
|
185
|
+
|
186
|
+
if (usingDataSource(`introspection`) && config.dataSources?.introspection) {
|
187
|
+
const introspectionConfig = {
|
188
|
+
projectRoot: config.projectRoot,
|
189
|
+
...config.dataSources.introspection,
|
190
|
+
}
|
191
|
+
const result = await DataSources.Introspection.readOrThrow(introspectionConfig)
|
192
|
+
if (result.data) return result
|
102
193
|
}
|
103
194
|
|
104
|
-
return null
|
195
|
+
return { data: null, source: { type: 'data' } }
|
105
196
|
}
|
@@ -157,13 +157,13 @@ export const createSchemaSource = (config: SchemaSourceConfig) => {
|
|
157
157
|
},
|
158
158
|
|
159
159
|
writeAllAssets: async (
|
160
|
-
schemaData: Awaited<ReturnType<typeof Schema.readOrThrow
|
160
|
+
schemaData: Awaited<ReturnType<typeof Schema.readOrThrow>>['data'],
|
161
161
|
metadata: Schema.SchemaMetadata,
|
162
162
|
) => {
|
163
163
|
if (!schemaData) return
|
164
164
|
|
165
165
|
// Write schema and changelog files
|
166
|
-
for (const [index, version] of schemaData
|
166
|
+
for (const [index, version] of schemaData!.entries()) {
|
167
167
|
const versionName = index === 0 ? Schema.VERSION_LATEST : Schema.dateToVersionString(version.date)
|
168
168
|
|
169
169
|
// Write schema file
|
@@ -173,7 +173,7 @@ export const createSchemaSource = (config: SchemaSourceConfig) => {
|
|
173
173
|
)
|
174
174
|
|
175
175
|
// Write changelog file (except for the oldest/last version)
|
176
|
-
if (Schema.shouldVersionHaveChangelog(index, schemaData
|
176
|
+
if (Schema.shouldVersionHaveChangelog(index, schemaData!.length)) {
|
177
177
|
const changelogData = {
|
178
178
|
changes: version.changes,
|
179
179
|
date: version.date.toISOString(),
|
@@ -35,15 +35,15 @@ export const Core = (config: Config.Config): Vite.PluginOption[] => {
|
|
35
35
|
|
36
36
|
const readSchema = async () => {
|
37
37
|
if (schemaCache === null) {
|
38
|
-
const
|
38
|
+
const schemaResult = await Schema.readOrThrow({
|
39
39
|
...config.schema,
|
40
40
|
projectRoot: config.paths.project.rootDir,
|
41
41
|
})
|
42
42
|
// todo: augmentations scoped to a version
|
43
|
-
|
43
|
+
schemaResult.data?.forEach(version => {
|
44
44
|
SchemaAugmentation.apply(version.after, config.schemaAugmentations)
|
45
45
|
})
|
46
|
-
schemaCache =
|
46
|
+
schemaCache = schemaResult
|
47
47
|
}
|
48
48
|
return schemaCache
|
49
49
|
}
|
@@ -166,8 +166,8 @@ export const Core = (config: Config.Config): Vite.PluginOption[] => {
|
|
166
166
|
const debug = debugPolen.sub(`module-project-schema`)
|
167
167
|
debug(`load`, { id: viProjectSchema.id })
|
168
168
|
|
169
|
-
const
|
170
|
-
return superjson.stringify(
|
169
|
+
const schemaResult = await readSchema()
|
170
|
+
return superjson.stringify(schemaResult.data)
|
171
171
|
},
|
172
172
|
},
|
173
173
|
{
|
@@ -191,18 +191,18 @@ export const Core = (config: Config.Config): Vite.PluginOption[] => {
|
|
191
191
|
|
192
192
|
debug(`load`, { id: viProjectData.id })
|
193
193
|
|
194
|
-
const
|
194
|
+
const schemaResult = await readSchema()
|
195
195
|
|
196
196
|
const navbar = []
|
197
197
|
|
198
198
|
// ━ Schema presence causes adding some navbar items
|
199
|
-
if (
|
199
|
+
if (schemaResult.data) {
|
200
200
|
// IMPORTANT: Always ensure paths start with '/' for React Router compatibility.
|
201
201
|
// Without the leading slash, React Router treats paths as relative, which causes
|
202
202
|
// hydration mismatches between SSR (where base path is prepended) and client
|
203
203
|
// (where basename is configured). This ensures consistent behavior.
|
204
204
|
navbar.push({ pathExp: `/reference`, title: `Reference` })
|
205
|
-
if (
|
205
|
+
if (schemaResult.data.length > 1) {
|
206
206
|
navbar.push({ pathExp: `/changelog`, title: `Changelog` })
|
207
207
|
}
|
208
208
|
}
|
@@ -9,6 +9,7 @@ import { debugPolen } from '#singletons/debug'
|
|
9
9
|
import { Cache } from '@wollybeard/kit'
|
10
10
|
import * as NodeFs from 'node:fs/promises'
|
11
11
|
import * as NodePath from 'node:path'
|
12
|
+
import type { NonEmptyChangeSets } from '../../schema/schema.js'
|
12
13
|
import { polenVirtual } from '../vi.js'
|
13
14
|
|
14
15
|
export const viProjectSchemaMetadata = polenVirtual([`project`, `schema-metadata`])
|
@@ -35,27 +36,28 @@ export const SchemaAssets = (config: Config.Config): Vite.Plugin => {
|
|
35
36
|
|
36
37
|
// Helper to load and process schema data
|
37
38
|
const loadAndProcessSchemaData = Cache.memoize(async () => {
|
38
|
-
const
|
39
|
+
const schemaResult = await Schema.readOrThrow({
|
39
40
|
...config.schema,
|
40
41
|
projectRoot: config.paths.project.rootDir,
|
41
42
|
})
|
42
43
|
|
43
|
-
if (!
|
44
|
+
if (!schemaResult.data) {
|
44
45
|
const metadata: Schema.SchemaMetadata = { hasSchema: false, versions: [] }
|
45
46
|
return {
|
46
47
|
schemaData: null,
|
47
48
|
metadata,
|
49
|
+
source: schemaResult.source,
|
48
50
|
}
|
49
51
|
}
|
50
52
|
|
51
53
|
// Apply augmentations
|
52
|
-
|
54
|
+
schemaResult.data.forEach(version => {
|
53
55
|
SchemaAugmentation.apply(version.after, config.schemaAugmentations)
|
54
56
|
})
|
55
57
|
|
56
58
|
// Build metadata
|
57
59
|
const versionStrings: string[] = []
|
58
|
-
for (const [index, version] of
|
60
|
+
for (const [index, version] of schemaResult.data.entries()) {
|
59
61
|
const versionName = index === 0 ? Schema.VERSION_LATEST : Schema.dateToVersionString(version.date)
|
60
62
|
versionStrings.push(versionName)
|
61
63
|
}
|
@@ -65,11 +67,12 @@ export const SchemaAssets = (config: Config.Config): Vite.Plugin => {
|
|
65
67
|
versions: versionStrings,
|
66
68
|
}
|
67
69
|
|
68
|
-
debug(`schemaDataLoaded`, { versionCount:
|
70
|
+
debug(`schemaDataLoaded`, { versionCount: schemaResult.data.length })
|
69
71
|
|
70
72
|
return {
|
71
|
-
schemaData,
|
73
|
+
schemaData: schemaResult.data,
|
72
74
|
metadata,
|
75
|
+
source: schemaResult.source,
|
73
76
|
}
|
74
77
|
})
|
75
78
|
|
@@ -102,10 +105,10 @@ export const SchemaAssets = (config: Config.Config): Vite.Plugin => {
|
|
102
105
|
|
103
106
|
// Helper to write assets using schema-source API
|
104
107
|
const writeDevAssets = async (
|
105
|
-
schemaData:
|
108
|
+
schemaData: NonEmptyChangeSets,
|
106
109
|
metadata: Schema.SchemaMetadata,
|
107
110
|
) => {
|
108
|
-
|
111
|
+
// schemaData is now guaranteed to be non-null NonEmptyChangeSets
|
109
112
|
|
110
113
|
const devAssetsDir = config.paths.framework.devAssets.schemas
|
111
114
|
await NodeFs.mkdir(devAssetsDir, { recursive: true })
|
@@ -149,6 +152,13 @@ export const SchemaAssets = (config: Config.Config): Vite.Plugin => {
|
|
149
152
|
debug(`watchingSchemaFile`, { path: config.schema.dataSources.file.path })
|
150
153
|
}
|
151
154
|
|
155
|
+
if (config.schema?.dataSources?.introspection?.url) {
|
156
|
+
// Watch the introspection file if introspection is configured
|
157
|
+
const introspectionFilePath = NodePath.join(config.paths.project.rootDir, `schema.introspection.json`)
|
158
|
+
server.watcher.add(introspectionFilePath)
|
159
|
+
debug(`watchingIntrospectionFile`, { path: introspectionFilePath })
|
160
|
+
}
|
161
|
+
|
152
162
|
// Handle file removal
|
153
163
|
server.watcher.on('unlink', async (file) => {
|
154
164
|
const isSchemaFile = config.schema && (() => {
|
@@ -172,6 +182,15 @@ export const SchemaAssets = (config: Config.Config): Vite.Plugin => {
|
|
172
182
|
if (absoluteFile.startsWith(absoluteSchemaDir + NodePath.sep)) return true
|
173
183
|
}
|
174
184
|
|
185
|
+
// Check if file is the introspection file
|
186
|
+
if (config.schema.dataSources?.introspection?.url) {
|
187
|
+
const absoluteIntrospectionFile = NodePath.resolve(
|
188
|
+
config.paths.project.rootDir,
|
189
|
+
`schema.introspection.json`,
|
190
|
+
)
|
191
|
+
if (absoluteFile === absoluteIntrospectionFile) return true
|
192
|
+
}
|
193
|
+
|
175
194
|
return false
|
176
195
|
})()
|
177
196
|
|
@@ -181,14 +200,33 @@ export const SchemaAssets = (config: Config.Config): Vite.Plugin => {
|
|
181
200
|
try {
|
182
201
|
// Clear cache and regenerate
|
183
202
|
loadAndProcessSchemaData.clear()
|
184
|
-
const { schemaData, metadata } = await loadAndProcessSchemaData()
|
185
|
-
|
186
|
-
|
203
|
+
const { schemaData, metadata, source } = await loadAndProcessSchemaData()
|
204
|
+
|
205
|
+
// If file was deleted but can be recreated, attempt recreation
|
206
|
+
if (!schemaData && source.reCreate) {
|
207
|
+
debug(`attemptingSchemaRecreation`, { sourceType: source.type })
|
208
|
+
try {
|
209
|
+
const recreatedData = await source.reCreate()
|
210
|
+
if (recreatedData) {
|
211
|
+
// Clear cache again and reload after recreation
|
212
|
+
loadAndProcessSchemaData.clear()
|
213
|
+
const reloadResult = await loadAndProcessSchemaData()
|
214
|
+
if (reloadResult.schemaData) {
|
215
|
+
await writeDevAssets(reloadResult.schemaData, reloadResult.metadata)
|
216
|
+
debug(`hmr:schemaRecreatedAndWritten`, { versionCount: reloadResult.schemaData.length })
|
217
|
+
}
|
218
|
+
} else {
|
219
|
+
debug(`hmr:schemaRecreationFailed`, { reason: 'reCreate returned null' })
|
220
|
+
}
|
221
|
+
} catch (recreationError) {
|
222
|
+
debug(`hmr:schemaRecreationFailed`, { error: recreationError })
|
223
|
+
}
|
224
|
+
} else if (schemaData) {
|
187
225
|
// Write new assets without the removed file
|
188
226
|
await writeDevAssets(schemaData, metadata)
|
189
227
|
debug(`hmr:schemaAssetsUpdatedAfterRemoval`, { versionCount: schemaData.length })
|
190
228
|
} else {
|
191
|
-
// No schema data - clear all assets
|
229
|
+
// No schema data and cannot recreate - clear all assets
|
192
230
|
const schemaSource = createDevSchemaSource({ hasSchema: false, versions: [] })
|
193
231
|
await schemaSource.clearAllAssets()
|
194
232
|
debug(`hmr:allAssetsCleared`, {})
|
@@ -274,6 +312,15 @@ export const SchemaAssets = (config: Config.Config): Vite.Plugin => {
|
|
274
312
|
if (absoluteFile.startsWith(absoluteSchemaDir + NodePath.sep)) return true
|
275
313
|
}
|
276
314
|
|
315
|
+
// Check if file is the introspection file
|
316
|
+
if (config.schema.dataSources?.introspection?.url) {
|
317
|
+
const absoluteIntrospectionFile = NodePath.resolve(
|
318
|
+
config.paths.project.rootDir,
|
319
|
+
`schema.introspection.json`,
|
320
|
+
)
|
321
|
+
if (absoluteFile === absoluteIntrospectionFile) return true
|
322
|
+
}
|
323
|
+
|
277
324
|
return false
|
278
325
|
})()
|
279
326
|
if (isSchemaFile) {
|
package/src/cli/commands/open.ts
CHANGED
@@ -132,7 +132,7 @@ const wrapCache = <fn extends Fn.AnyAnyAsync>(fn: fn): fn => {
|
|
132
132
|
|
133
133
|
// todo: use a proper validation, e.g. zod, better yet: allow to specify the validation in molt itself
|
134
134
|
const parseHeaders = (headersJsonEncoded: string): Record<string, string> => {
|
135
|
-
const headersJson = Json.codec.
|
135
|
+
const headersJson = Json.codec.decode(headersJsonEncoded)
|
136
136
|
if (!Rec.is(headersJson)) {
|
137
137
|
console.log(`--introspection-headers must be a JSON object.`)
|
138
138
|
process.exit(1)
|
package/src/lib/kit-temp.ts
CHANGED
@@ -105,9 +105,9 @@ export const objPolicyFilter = <
|
|
105
105
|
const result: any = mode === `deny` ? { ...obj } : {}
|
106
106
|
|
107
107
|
if (mode === `allow`) {
|
108
|
-
// For allow mode, only add specified keys
|
108
|
+
// For allow mode, only add specified keys that are own properties
|
109
109
|
for (const key of keys) {
|
110
|
-
if (key
|
110
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
111
111
|
// @ts-expect-error
|
112
112
|
result[key] = obj[key]
|
113
113
|
}
|
package/src/lib/mask/$.test.ts
CHANGED
@@ -113,9 +113,9 @@ describe('property-based tests', () => {
|
|
113
113
|
// Result contains only keys that were in both mask and object
|
114
114
|
expect(resultKeys.every(key => keys.includes(key))).toBe(true)
|
115
115
|
|
116
|
-
// All requested keys that
|
116
|
+
// All requested keys that are own properties of obj are in result
|
117
117
|
keys.forEach(key => {
|
118
|
-
if (key
|
118
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
119
119
|
expect(result).toHaveProperty(key, (obj as any)[key])
|
120
120
|
}
|
121
121
|
})
|
@@ -169,7 +169,7 @@ describe('property-based tests', () => {
|
|
169
169
|
const result = Mask.apply(obj as any, Mask.create(keys))
|
170
170
|
|
171
171
|
keys.forEach(key => {
|
172
|
-
if (key
|
172
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
173
173
|
expect(result).toHaveProperty(key)
|
174
174
|
expect((result as any)[key]).toBe((obj as any)[key])
|
175
175
|
}
|