@effect-gql/persisted-queries 0.1.0 → 1.0.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/README.md +100 -0
- package/index.cjs +385 -0
- package/index.cjs.map +1 -0
- package/index.d.cts +403 -0
- package/index.d.ts +403 -0
- package/index.js +372 -0
- package/index.js.map +1 -0
- package/package.json +14 -26
- package/dist/config.d.ts +0 -106
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -27
- package/dist/config.js.map +0 -1
- package/dist/errors.d.ts +0 -77
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -77
- package/dist/errors.js.map +0 -1
- package/dist/index.d.ts +0 -56
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -71
- package/dist/index.js.map +0 -1
- package/dist/memory-store.d.ts +0 -67
- package/dist/memory-store.d.ts.map +0 -1
- package/dist/memory-store.js +0 -104
- package/dist/memory-store.js.map +0 -1
- package/dist/persisted-queries-router.d.ts +0 -58
- package/dist/persisted-queries-router.d.ts.map +0 -1
- package/dist/persisted-queries-router.js +0 -277
- package/dist/persisted-queries-router.js.map +0 -1
- package/dist/store.d.ts +0 -49
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -29
- package/dist/store.js.map +0 -1
- package/dist/utils.d.ts +0 -51
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -73
- package/dist/utils.js.map +0 -1
- package/src/config.ts +0 -122
- package/src/errors.ts +0 -107
- package/src/index.ts +0 -77
- package/src/memory-store.ts +0 -133
- package/src/persisted-queries-router.ts +0 -421
- package/src/store.ts +0 -54
- package/src/utils.ts +0 -111
package/src/errors.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { Data } from "effect"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* GraphQL error format for APQ responses.
|
|
5
|
-
* Compatible with Apollo Client's error handling.
|
|
6
|
-
*/
|
|
7
|
-
export interface PersistedQueryGraphQLError {
|
|
8
|
-
readonly message: string
|
|
9
|
-
readonly extensions: {
|
|
10
|
-
readonly code: string
|
|
11
|
-
readonly [key: string]: unknown
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Error returned when a persisted query hash is not found in the store
|
|
17
|
-
* and no query body was provided.
|
|
18
|
-
*
|
|
19
|
-
* Apollo clients recognize this error and automatically retry with the full query.
|
|
20
|
-
* This is the expected flow for Automatic Persisted Queries (APQ).
|
|
21
|
-
*/
|
|
22
|
-
export class PersistedQueryNotFoundError extends Data.TaggedError("PersistedQueryNotFoundError")<{
|
|
23
|
-
readonly hash: string
|
|
24
|
-
}> {
|
|
25
|
-
/**
|
|
26
|
-
* Convert to GraphQL error format compatible with Apollo protocol.
|
|
27
|
-
*/
|
|
28
|
-
toGraphQLError(): PersistedQueryGraphQLError {
|
|
29
|
-
return {
|
|
30
|
-
message: "PersistedQueryNotFound",
|
|
31
|
-
extensions: {
|
|
32
|
-
code: "PERSISTED_QUERY_NOT_FOUND",
|
|
33
|
-
},
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Error returned when the persisted query protocol version is not supported.
|
|
40
|
-
*
|
|
41
|
-
* Currently only version 1 is supported, which uses SHA-256 hashing.
|
|
42
|
-
*/
|
|
43
|
-
export class PersistedQueryVersionError extends Data.TaggedError("PersistedQueryVersionError")<{
|
|
44
|
-
readonly version: number
|
|
45
|
-
}> {
|
|
46
|
-
toGraphQLError(): PersistedQueryGraphQLError {
|
|
47
|
-
return {
|
|
48
|
-
message: `Unsupported persisted query version: ${this.version}`,
|
|
49
|
-
extensions: {
|
|
50
|
-
code: "PERSISTED_QUERY_VERSION_NOT_SUPPORTED",
|
|
51
|
-
version: this.version,
|
|
52
|
-
},
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Error returned when the provided query doesn't match its hash.
|
|
59
|
-
*
|
|
60
|
-
* This can indicate a client bug or a potential hash collision attack.
|
|
61
|
-
* Hash validation is enabled by default and can be disabled if needed.
|
|
62
|
-
*/
|
|
63
|
-
export class PersistedQueryHashMismatchError extends Data.TaggedError(
|
|
64
|
-
"PersistedQueryHashMismatchError"
|
|
65
|
-
)<{
|
|
66
|
-
readonly providedHash: string
|
|
67
|
-
readonly computedHash: string
|
|
68
|
-
}> {
|
|
69
|
-
toGraphQLError(): PersistedQueryGraphQLError {
|
|
70
|
-
return {
|
|
71
|
-
message: "Query hash does not match provided hash",
|
|
72
|
-
extensions: {
|
|
73
|
-
code: "PERSISTED_QUERY_HASH_MISMATCH",
|
|
74
|
-
},
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Error returned when trying to execute a query that is not in the safelist.
|
|
81
|
-
*
|
|
82
|
-
* In safelist mode, only pre-registered queries are allowed.
|
|
83
|
-
* This error is returned when a client tries to register a new query.
|
|
84
|
-
*/
|
|
85
|
-
export class PersistedQueryNotAllowedError extends Data.TaggedError(
|
|
86
|
-
"PersistedQueryNotAllowedError"
|
|
87
|
-
)<{
|
|
88
|
-
readonly hash: string
|
|
89
|
-
}> {
|
|
90
|
-
toGraphQLError(): PersistedQueryGraphQLError {
|
|
91
|
-
return {
|
|
92
|
-
message: "Query not in safelist",
|
|
93
|
-
extensions: {
|
|
94
|
-
code: "PERSISTED_QUERY_NOT_ALLOWED",
|
|
95
|
-
},
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Union type of all APQ-related errors
|
|
102
|
-
*/
|
|
103
|
-
export type PersistedQueryError =
|
|
104
|
-
| PersistedQueryNotFoundError
|
|
105
|
-
| PersistedQueryVersionError
|
|
106
|
-
| PersistedQueryHashMismatchError
|
|
107
|
-
| PersistedQueryNotAllowedError
|
package/src/index.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @effect-gql/persisted-queries
|
|
3
|
-
*
|
|
4
|
-
* Apollo Persisted Queries support for Effect GraphQL.
|
|
5
|
-
*
|
|
6
|
-
* Supports both Automatic Persisted Queries (APQ) mode for runtime registration
|
|
7
|
-
* and Safelist mode for pre-registered query allowlisting.
|
|
8
|
-
*
|
|
9
|
-
* ## Quick Start
|
|
10
|
-
*
|
|
11
|
-
* @example APQ Mode (runtime registration)
|
|
12
|
-
* ```typescript
|
|
13
|
-
* import { makePersistedQueriesRouter } from "@effect-gql/persisted-queries"
|
|
14
|
-
*
|
|
15
|
-
* const router = makePersistedQueriesRouter(schema, serviceLayer, {
|
|
16
|
-
* mode: "apq",
|
|
17
|
-
* enableGet: true, // Enable CDN caching
|
|
18
|
-
* graphiql: { path: "/graphiql" },
|
|
19
|
-
* })
|
|
20
|
-
* ```
|
|
21
|
-
*
|
|
22
|
-
* @example Safelist Mode (pre-registered queries only)
|
|
23
|
-
* ```typescript
|
|
24
|
-
* import { makePersistedQueriesRouter, makeSafelistStore } from "@effect-gql/persisted-queries"
|
|
25
|
-
*
|
|
26
|
-
* const router = makePersistedQueriesRouter(schema, serviceLayer, {
|
|
27
|
-
* mode: "safelist",
|
|
28
|
-
* store: makeSafelistStore({
|
|
29
|
-
* "ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38": "query GetUser($id: ID!) { user(id: $id) { name email } }",
|
|
30
|
-
* "a1b2c3d4...": "query GetPosts { posts { title } }",
|
|
31
|
-
* }),
|
|
32
|
-
* })
|
|
33
|
-
* ```
|
|
34
|
-
*
|
|
35
|
-
* @example Custom Store Size
|
|
36
|
-
* ```typescript
|
|
37
|
-
* import { makePersistedQueriesRouter, makeMemoryStore } from "@effect-gql/persisted-queries"
|
|
38
|
-
*
|
|
39
|
-
* const router = makePersistedQueriesRouter(schema, serviceLayer, {
|
|
40
|
-
* store: makeMemoryStore({ maxSize: 5000 }),
|
|
41
|
-
* })
|
|
42
|
-
* ```
|
|
43
|
-
*
|
|
44
|
-
* @packageDocumentation
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
// Router
|
|
48
|
-
export { makePersistedQueriesRouter } from "./persisted-queries-router"
|
|
49
|
-
|
|
50
|
-
// Store interface and implementations
|
|
51
|
-
export { PersistedQueryStore } from "./store"
|
|
52
|
-
export type { PersistedQueryStore as PersistedQueryStoreInterface } from "./store"
|
|
53
|
-
export { makeMemoryStore, makeSafelistStore } from "./memory-store"
|
|
54
|
-
export type { MemoryStoreConfig } from "./memory-store"
|
|
55
|
-
|
|
56
|
-
// Configuration
|
|
57
|
-
export type {
|
|
58
|
-
PersistedQueriesConfig,
|
|
59
|
-
PersistedQueriesRouterOptions,
|
|
60
|
-
PersistedQueryMode,
|
|
61
|
-
HashAlgorithm,
|
|
62
|
-
} from "./config"
|
|
63
|
-
export { PersistedQueriesConfigFromEnv } from "./config"
|
|
64
|
-
|
|
65
|
-
// Errors
|
|
66
|
-
export {
|
|
67
|
-
PersistedQueryNotFoundError,
|
|
68
|
-
PersistedQueryVersionError,
|
|
69
|
-
PersistedQueryHashMismatchError,
|
|
70
|
-
PersistedQueryNotAllowedError,
|
|
71
|
-
type PersistedQueryError,
|
|
72
|
-
type PersistedQueryGraphQLError,
|
|
73
|
-
} from "./errors"
|
|
74
|
-
|
|
75
|
-
// Utilities
|
|
76
|
-
export { computeHash, parsePersistedQueryExtension, parseGetRequestBody } from "./utils"
|
|
77
|
-
export type { PersistedQueryExtension, GraphQLRequestBody } from "./utils"
|
package/src/memory-store.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { Effect, Layer, Option } from "effect"
|
|
2
|
-
import { PersistedQueryStore } from "./store"
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Configuration for the in-memory LRU store
|
|
6
|
-
*/
|
|
7
|
-
export interface MemoryStoreConfig {
|
|
8
|
-
/**
|
|
9
|
-
* Maximum number of queries to cache.
|
|
10
|
-
* When exceeded, least recently used queries are evicted.
|
|
11
|
-
* Default: 1000
|
|
12
|
-
*/
|
|
13
|
-
readonly maxSize?: number
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Create an in-memory LRU (Least Recently Used) store for persisted queries.
|
|
18
|
-
*
|
|
19
|
-
* This implementation uses Map's natural insertion order for O(1) LRU operations:
|
|
20
|
-
* - get: O(1) - delete and re-insert to move to end (most recently used)
|
|
21
|
-
* - set: O(1) - insert at end, evict from front if needed
|
|
22
|
-
* - eviction: O(1) - delete first entry (least recently used)
|
|
23
|
-
*
|
|
24
|
-
* This is the default store implementation suitable for single-instance servers.
|
|
25
|
-
* For multi-instance deployments, consider using a shared store like Redis.
|
|
26
|
-
*
|
|
27
|
-
* @param config - Optional configuration for cache size
|
|
28
|
-
* @returns A Layer providing the PersistedQueryStore service
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* ```typescript
|
|
32
|
-
* import { makeMemoryStore, makePersistedQueriesRouter } from "@effect-gql/persisted-queries"
|
|
33
|
-
*
|
|
34
|
-
* // Default store with 1000 entry limit
|
|
35
|
-
* const router1 = makePersistedQueriesRouter(schema, serviceLayer)
|
|
36
|
-
*
|
|
37
|
-
* // Custom store with larger cache
|
|
38
|
-
* const router2 = makePersistedQueriesRouter(schema, serviceLayer, {
|
|
39
|
-
* store: makeMemoryStore({ maxSize: 5000 })
|
|
40
|
-
* })
|
|
41
|
-
* ```
|
|
42
|
-
*/
|
|
43
|
-
export const makeMemoryStore = (
|
|
44
|
-
config: MemoryStoreConfig = {}
|
|
45
|
-
): Layer.Layer<PersistedQueryStore> => {
|
|
46
|
-
const maxSize = config.maxSize ?? 1000
|
|
47
|
-
|
|
48
|
-
// Map maintains insertion order - we use this for O(1) LRU
|
|
49
|
-
// First entry = least recently used, last entry = most recently used
|
|
50
|
-
const cache = new Map<string, string>()
|
|
51
|
-
|
|
52
|
-
// Move entry to end (most recently used) by deleting and re-inserting
|
|
53
|
-
const touch = (hash: string, query: string): void => {
|
|
54
|
-
cache.delete(hash)
|
|
55
|
-
cache.set(hash, query)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Evict oldest entry (first in Map) if over capacity - O(1)
|
|
59
|
-
const evictIfNeeded = (): void => {
|
|
60
|
-
if (cache.size <= maxSize) return
|
|
61
|
-
// Map.keys().next() gives us the first (oldest) key in O(1)
|
|
62
|
-
const oldestKey = cache.keys().next().value
|
|
63
|
-
if (oldestKey !== undefined) {
|
|
64
|
-
cache.delete(oldestKey)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return Layer.succeed(
|
|
69
|
-
PersistedQueryStore,
|
|
70
|
-
PersistedQueryStore.of({
|
|
71
|
-
get: (hash) =>
|
|
72
|
-
Effect.sync(() => {
|
|
73
|
-
const query = cache.get(hash)
|
|
74
|
-
if (query === undefined) {
|
|
75
|
-
return Option.none<string>()
|
|
76
|
-
}
|
|
77
|
-
// Move to end (most recently used)
|
|
78
|
-
touch(hash, query)
|
|
79
|
-
return Option.some(query)
|
|
80
|
-
}),
|
|
81
|
-
|
|
82
|
-
set: (hash, query) =>
|
|
83
|
-
Effect.sync(() => {
|
|
84
|
-
// If key exists, delete first to ensure it moves to end
|
|
85
|
-
cache.delete(hash)
|
|
86
|
-
cache.set(hash, query)
|
|
87
|
-
evictIfNeeded()
|
|
88
|
-
}),
|
|
89
|
-
|
|
90
|
-
has: (hash) => Effect.sync(() => cache.has(hash)),
|
|
91
|
-
})
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Create a pre-populated safelist store.
|
|
97
|
-
*
|
|
98
|
-
* This store only allows queries that were provided at creation time.
|
|
99
|
-
* Any attempt to store new queries is silently ignored.
|
|
100
|
-
* Use this for production security where you want to allowlist specific operations.
|
|
101
|
-
*
|
|
102
|
-
* @param queries - Record mapping SHA-256 hashes to query strings
|
|
103
|
-
* @returns A Layer providing the PersistedQueryStore service
|
|
104
|
-
*
|
|
105
|
-
* @example
|
|
106
|
-
* ```typescript
|
|
107
|
-
* import { makeSafelistStore, makePersistedQueriesRouter } from "@effect-gql/persisted-queries"
|
|
108
|
-
*
|
|
109
|
-
* // Pre-register allowed queries
|
|
110
|
-
* const router = makePersistedQueriesRouter(schema, serviceLayer, {
|
|
111
|
-
* mode: "safelist",
|
|
112
|
-
* store: makeSafelistStore({
|
|
113
|
-
* "ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38": "query GetUser($id: ID!) { user(id: $id) { name email } }",
|
|
114
|
-
* "a1b2c3d4...": "query GetPosts { posts { title } }",
|
|
115
|
-
* }),
|
|
116
|
-
* })
|
|
117
|
-
* ```
|
|
118
|
-
*/
|
|
119
|
-
export const makeSafelistStore = (
|
|
120
|
-
queries: Record<string, string>
|
|
121
|
-
): Layer.Layer<PersistedQueryStore> =>
|
|
122
|
-
Layer.succeed(
|
|
123
|
-
PersistedQueryStore,
|
|
124
|
-
PersistedQueryStore.of({
|
|
125
|
-
get: (hash) =>
|
|
126
|
-
Effect.succeed(queries[hash] !== undefined ? Option.some(queries[hash]) : Option.none()),
|
|
127
|
-
|
|
128
|
-
// No-op for safelist mode - queries cannot be added at runtime
|
|
129
|
-
set: () => Effect.void,
|
|
130
|
-
|
|
131
|
-
has: (hash) => Effect.succeed(queries[hash] !== undefined),
|
|
132
|
-
})
|
|
133
|
-
)
|