@opensaas/stack-core 0.19.1 → 0.20.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.
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Unwrap the item type from a field type, stripping null, undefined, and
3
+ * Array wrappers so we can constrain nested fragment shapes.
4
+ *
5
+ * Examples:
6
+ * User | null → User
7
+ * User[] → User
8
+ * (User | null)[] → User
9
+ */
10
+ type UnwrapItem<T> = NonNullable<T> extends Array<infer U> ? NonNullable<U> : NonNullable<T>;
11
+ /**
12
+ * A selector for a relationship field.
13
+ *
14
+ * Two forms are accepted:
15
+ * 1. A `Fragment` directly (shorthand — no extra Prisma args on the nested query).
16
+ * 2. An object `{ query, where?, orderBy?, take?, skip? }` to combine a fragment
17
+ * with Prisma filter/ordering/pagination applied to the nested relationship.
18
+ *
19
+ * @example Shorthand (most common)
20
+ * ```ts
21
+ * const postFrag = defineFragment<Post>()({
22
+ * id: true,
23
+ * author: authorFragment, // shorthand
24
+ * } as const)
25
+ * ```
26
+ *
27
+ * @example With nested filtering
28
+ * ```ts
29
+ * const postFrag = defineFragment<Post>()({
30
+ * id: true,
31
+ * comments: {
32
+ * query: commentFragment,
33
+ * where: { approved: true },
34
+ * orderBy: { createdAt: 'desc' },
35
+ * take: 5,
36
+ * },
37
+ * } as const)
38
+ * ```
39
+ *
40
+ * @example Variables via factory function
41
+ * ```ts
42
+ * function makePostFragment(status: string) {
43
+ * return defineFragment<Post>()({
44
+ * id: true,
45
+ * comments: { query: commentFragment, where: { status } },
46
+ * } as const)
47
+ * }
48
+ * type PostData = ResultOf<ReturnType<typeof makePostFragment>>
49
+ * ```
50
+ */
51
+ export type RelationSelector<TRelated extends Record<string, unknown>> = Fragment<TRelated, FieldSelection<TRelated>> | {
52
+ readonly query: Fragment<TRelated, FieldSelection<TRelated>>;
53
+ readonly where?: Record<string, unknown>;
54
+ readonly orderBy?: Record<string, 'asc' | 'desc'> | Array<Record<string, 'asc' | 'desc'>>;
55
+ readonly take?: number;
56
+ readonly skip?: number;
57
+ };
58
+ /**
59
+ * A field selection for model type `TItem`.
60
+ *
61
+ * Each key maps to:
62
+ * - `true` — include the scalar/primitive field as-is
63
+ * - A `Fragment` — include a relationship and recurse (shorthand)
64
+ * - A `RelationSelector` — include a relationship with optional Prisma filter/ordering
65
+ *
66
+ * Only keys present in `TItem` are accepted. For relationship (object) fields
67
+ * you may pass a Fragment, a RelationSelector, or `true` (returns the raw Prisma
68
+ * value and loses type narrowing).
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * const sel: FieldSelection<Post> = {
73
+ * id: true,
74
+ * title: true,
75
+ * author: authorFragment,
76
+ * comments: { query: commentFragment, where: { approved: true } },
77
+ * }
78
+ * ```
79
+ */
80
+ export type FieldSelection<T> = {
81
+ readonly [K in keyof T]?: UnwrapItem<T[K]> extends Record<string, unknown> ? RelationSelector<UnwrapItem<T[K]>> | true : true;
82
+ };
83
+ /**
84
+ * A reusable, composable field-selection descriptor for model type `TItem`.
85
+ *
86
+ * Create with {@link defineFragment}. Compose by referencing another Fragment
87
+ * (or a {@link RelationSelector}) as the value for a relationship key.
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * const userFragment = defineFragment<User>()({ id: true, name: true } as const)
92
+ * const postFragment = defineFragment<Post>()({
93
+ * id: true,
94
+ * title: true,
95
+ * author: userFragment,
96
+ * } as const)
97
+ * ```
98
+ */
99
+ export type Fragment<TItem, TFields extends FieldSelection<TItem> = FieldSelection<TItem>> = {
100
+ readonly _type: 'fragment';
101
+ readonly _fields: TFields;
102
+ };
103
+ /**
104
+ * @internal
105
+ * Extract the Fragment from either a Fragment directly or a RelationSelector object.
106
+ * Returns `never` for scalar `true` selections (so they fall to the scalar branch).
107
+ */
108
+ type ExtractFragment<TSelector> = TSelector extends Fragment<infer TItem, infer TFields> ? Fragment<TItem, TFields> : TSelector extends {
109
+ readonly query: Fragment<infer TItem, infer TFields>;
110
+ } ? Fragment<TItem, TFields> : never;
111
+ /**
112
+ * @internal
113
+ * Map a FieldSelection over a model type, computing the picked output type.
114
+ */
115
+ type SelectedFields<TItem, TFields extends FieldSelection<TItem>> = {
116
+ [K in keyof TFields & keyof TItem]: [ExtractFragment<TFields[K]>] extends [never] ? TItem[K] : TItem[K] extends Array<unknown> ? ResultOf<ExtractFragment<TFields[K]>>[] : null extends TItem[K] ? ResultOf<ExtractFragment<TFields[K]>> | null : undefined extends TItem[K] ? ResultOf<ExtractFragment<TFields[K]>> | undefined : ResultOf<ExtractFragment<TFields[K]>>;
117
+ };
118
+ /**
119
+ * Infer the TypeScript result type from a Fragment.
120
+ *
121
+ * Analogous to `gql.tada`'s `ResultOf` helper — given a fragment definition,
122
+ * `ResultOf` tells you exactly what shape you will receive at runtime.
123
+ *
124
+ * - Scalar fields selected with `true` retain their original Prisma type.
125
+ * - Relationship fields selected with a nested Fragment/RelationSelector are
126
+ * recursively narrowed.
127
+ * - Nullability and array wrappers from the original model type are preserved.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * type UserData = ResultOf<typeof userFragment>
132
+ * // → { id: string; name: string }
133
+ *
134
+ * type PostData = ResultOf<typeof postFragment>
135
+ * // → { id: string; title: string; author: { id: string; name: string } | null }
136
+ * ```
137
+ */
138
+ export type ResultOf<F> = F extends Fragment<infer TItem, infer TFields> ? SelectedFields<TItem, TFields> : never;
139
+ /**
140
+ * Arguments accepted by {@link runQuery}.
141
+ */
142
+ export type QueryArgs = {
143
+ /** Prisma where filter. The access control layer will additionally scope results. */
144
+ where?: Record<string, unknown>;
145
+ /** Prisma orderBy clause. Pass a single object or an array for multi-column ordering. */
146
+ orderBy?: Record<string, 'asc' | 'desc'> | Array<Record<string, 'asc' | 'desc'>>;
147
+ /** Maximum number of records to return. */
148
+ take?: number;
149
+ /** Number of records to skip (for pagination). */
150
+ skip?: number;
151
+ };
152
+ /**
153
+ * Minimal context shape required by the query runners.
154
+ * Compatible with the full `AccessContext` produced by `getContext()`.
155
+ */
156
+ export interface QueryRunnerContext {
157
+ db: {
158
+ [key: string]: {
159
+ findMany: (args?: unknown) => Promise<unknown[]>;
160
+ findFirst: (args?: unknown) => Promise<unknown>;
161
+ };
162
+ };
163
+ }
164
+ /**
165
+ * Create a type-safe, reusable fragment for a given model type.
166
+ *
167
+ * The function is curried so that TypeScript can infer both the model type
168
+ * (from the explicit type parameter) and the field selection (from the
169
+ * argument), without requiring you to repeat yourself.
170
+ *
171
+ * @example Basic usage
172
+ * ```ts
173
+ * import type { User } from '.prisma/client'
174
+ * import { defineFragment } from '@opensaas/stack-core'
175
+ *
176
+ * export const userFragment = defineFragment<User>()({
177
+ * id: true,
178
+ * name: true,
179
+ * email: true,
180
+ * } as const)
181
+ * ```
182
+ *
183
+ * @example Compose fragments
184
+ * ```ts
185
+ * import type { Post } from '.prisma/client'
186
+ *
187
+ * export const postFragment = defineFragment<Post>()({
188
+ * id: true,
189
+ * title: true,
190
+ * author: userFragment,
191
+ * } as const)
192
+ * ```
193
+ *
194
+ * @example Nested filtering with RelationSelector
195
+ * ```ts
196
+ * export const postWithApprovedComments = defineFragment<Post>()({
197
+ * id: true,
198
+ * title: true,
199
+ * comments: {
200
+ * query: commentFragment,
201
+ * where: { approved: true },
202
+ * orderBy: { createdAt: 'desc' },
203
+ * take: 5,
204
+ * },
205
+ * } as const)
206
+ * ```
207
+ *
208
+ * @example Variables via factory function
209
+ * ```ts
210
+ * function makePostFragment(status: string) {
211
+ * return defineFragment<Post>()({
212
+ * id: true,
213
+ * comments: { query: commentFragment, where: { status } },
214
+ * } as const)
215
+ * }
216
+ * type PostData = ResultOf<ReturnType<typeof makePostFragment>>
217
+ *
218
+ * const posts = await context.db.post.findMany({
219
+ * query: makePostFragment('approved'),
220
+ * where: { published: true },
221
+ * })
222
+ * ```
223
+ */
224
+ export declare function defineFragment<TItem>(): <TFields extends FieldSelection<TItem>>(fields: TFields) => Fragment<TItem, TFields>;
225
+ /** @internal */
226
+ export declare function isFragment(value: unknown): value is Fragment<unknown, FieldSelection<unknown>>;
227
+ /**
228
+ * Walk a field selection and build the Prisma `include` map needed to eagerly
229
+ * load all nested relationship fragments/selectors.
230
+ *
231
+ * Scalar fields (`true`) do not require an include entry — Prisma returns all
232
+ * scalar columns by default. Only relationship fields backed by a Fragment or
233
+ * RelationSelector generate include entries (recursively).
234
+ *
235
+ * Exported for use in `context/index.ts` when the `query` parameter is present.
236
+ * @internal
237
+ */
238
+ export declare function buildInclude(fields: FieldSelection<unknown>): Record<string, unknown> | undefined;
239
+ /**
240
+ * Recursively pick only the fields requested by a fragment from a raw Prisma
241
+ * result object. This ensures the runtime shape exactly matches the type
242
+ * produced by `ResultOf<F>`.
243
+ *
244
+ * Exported for use in `context/index.ts`.
245
+ * @internal
246
+ */
247
+ export declare function pickFields<TItem, TFields extends FieldSelection<TItem>>(item: TItem, fields: TFields): SelectedFields<TItem, TFields>;
248
+ /**
249
+ * Execute a fragment-based query against a list, returning all matching
250
+ * records shaped to the fragment's field selection.
251
+ *
252
+ * Under the hood this calls `context.db[listKey].findMany()`, so all access
253
+ * control rules defined in your config are still enforced.
254
+ *
255
+ * **Tip:** You can also call `context.db.post.findMany({ query: fragment, ... })`
256
+ * directly — both forms produce the same result.
257
+ *
258
+ * @param context - An `AccessContext` (or any object with a compatible `db`).
259
+ * @param listKey - The PascalCase list name (e.g. `'Post'`, `'BlogPost'`).
260
+ * @param fragment - A fragment created with {@link defineFragment}.
261
+ * @param args - Optional query arguments (where, orderBy, take, skip).
262
+ * @returns An array typed to exactly the fragment's field selection.
263
+ *
264
+ * @example
265
+ * ```ts
266
+ * const posts = await runQuery(context, 'Post', postFragment, {
267
+ * where: { published: true },
268
+ * orderBy: { createdAt: 'desc' },
269
+ * take: 10,
270
+ * })
271
+ * // posts: Array<ResultOf<typeof postFragment>>
272
+ * ```
273
+ */
274
+ export declare function runQuery<TItem, TFields extends FieldSelection<TItem>>(context: QueryRunnerContext, listKey: string, fragment: Fragment<TItem, TFields>, args?: QueryArgs): Promise<SelectedFields<TItem, TFields>[]>;
275
+ /**
276
+ * Execute a fragment-based query that returns a single record (or `null`).
277
+ *
278
+ * Under the hood this calls `context.db[listKey].findFirst()`, so all access
279
+ * control rules are still enforced.
280
+ *
281
+ * **Tip:** You can also call `context.db.post.findUnique({ where: { id }, query: fragment })`
282
+ * directly.
283
+ *
284
+ * @param context - An `AccessContext` (or any object with a compatible `db`).
285
+ * @param listKey - The PascalCase list name (e.g. `'Post'`).
286
+ * @param fragment - A fragment created with {@link defineFragment}.
287
+ * @param where - A Prisma where clause to identify the record.
288
+ * @returns The matched record shaped to the fragment, or `null`.
289
+ *
290
+ * @example
291
+ * ```ts
292
+ * const post = await runQueryOne(context, 'Post', postFragment, { id: postId })
293
+ * if (!post) return notFound()
294
+ * // post: ResultOf<typeof postFragment>
295
+ * ```
296
+ */
297
+ export declare function runQueryOne<TItem, TFields extends FieldSelection<TItem>>(context: QueryRunnerContext, listKey: string, fragment: Fragment<TItem, TFields>, where: Record<string, unknown>): Promise<SelectedFields<TItem, TFields> | null>;
298
+ export {};
299
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/query/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,255 @@
1
+ import { getDbKey } from '../lib/case-utils.js';
2
+ // ─────────────────────────────────────────────────────────────
3
+ // Fragment factory
4
+ // ─────────────────────────────────────────────────────────────
5
+ /**
6
+ * Create a type-safe, reusable fragment for a given model type.
7
+ *
8
+ * The function is curried so that TypeScript can infer both the model type
9
+ * (from the explicit type parameter) and the field selection (from the
10
+ * argument), without requiring you to repeat yourself.
11
+ *
12
+ * @example Basic usage
13
+ * ```ts
14
+ * import type { User } from '.prisma/client'
15
+ * import { defineFragment } from '@opensaas/stack-core'
16
+ *
17
+ * export const userFragment = defineFragment<User>()({
18
+ * id: true,
19
+ * name: true,
20
+ * email: true,
21
+ * } as const)
22
+ * ```
23
+ *
24
+ * @example Compose fragments
25
+ * ```ts
26
+ * import type { Post } from '.prisma/client'
27
+ *
28
+ * export const postFragment = defineFragment<Post>()({
29
+ * id: true,
30
+ * title: true,
31
+ * author: userFragment,
32
+ * } as const)
33
+ * ```
34
+ *
35
+ * @example Nested filtering with RelationSelector
36
+ * ```ts
37
+ * export const postWithApprovedComments = defineFragment<Post>()({
38
+ * id: true,
39
+ * title: true,
40
+ * comments: {
41
+ * query: commentFragment,
42
+ * where: { approved: true },
43
+ * orderBy: { createdAt: 'desc' },
44
+ * take: 5,
45
+ * },
46
+ * } as const)
47
+ * ```
48
+ *
49
+ * @example Variables via factory function
50
+ * ```ts
51
+ * function makePostFragment(status: string) {
52
+ * return defineFragment<Post>()({
53
+ * id: true,
54
+ * comments: { query: commentFragment, where: { status } },
55
+ * } as const)
56
+ * }
57
+ * type PostData = ResultOf<ReturnType<typeof makePostFragment>>
58
+ *
59
+ * const posts = await context.db.post.findMany({
60
+ * query: makePostFragment('approved'),
61
+ * where: { published: true },
62
+ * })
63
+ * ```
64
+ */
65
+ export function defineFragment() {
66
+ return function (fields) {
67
+ return { _type: 'fragment', _fields: fields };
68
+ };
69
+ }
70
+ // ─────────────────────────────────────────────────────────────
71
+ // Runtime helpers — exported for use in context/index.ts
72
+ // ─────────────────────────────────────────────────────────────
73
+ /** @internal */
74
+ export function isFragment(value) {
75
+ return (value !== null &&
76
+ typeof value === 'object' &&
77
+ '_type' in value &&
78
+ value._type === 'fragment');
79
+ }
80
+ /**
81
+ * Walk a field selection and build the Prisma `include` map needed to eagerly
82
+ * load all nested relationship fragments/selectors.
83
+ *
84
+ * Scalar fields (`true`) do not require an include entry — Prisma returns all
85
+ * scalar columns by default. Only relationship fields backed by a Fragment or
86
+ * RelationSelector generate include entries (recursively).
87
+ *
88
+ * Exported for use in `context/index.ts` when the `query` parameter is present.
89
+ * @internal
90
+ */
91
+ export function buildInclude(fields) {
92
+ const include = {};
93
+ let hasIncludes = false;
94
+ for (const [key, value] of Object.entries(fields)) {
95
+ if (value === null || value === true || typeof value !== 'object')
96
+ continue;
97
+ const val = value;
98
+ // ── Shorthand: Fragment directly ──────────────────────────
99
+ if (isFragment(val)) {
100
+ hasIncludes = true;
101
+ const nestedInclude = buildInclude(val._fields);
102
+ include[key] = nestedInclude ? { include: nestedInclude } : true;
103
+ continue;
104
+ }
105
+ // ── RelationSelector: { query, where?, orderBy?, take?, skip? } ──
106
+ if ('query' in val && isFragment(val.query)) {
107
+ hasIncludes = true;
108
+ const selector = val;
109
+ const nestedInclude = buildInclude(selector.query._fields);
110
+ const includeEntry = {};
111
+ if (selector.where !== undefined)
112
+ includeEntry.where = selector.where;
113
+ if (selector.orderBy !== undefined)
114
+ includeEntry.orderBy = selector.orderBy;
115
+ if (selector.take !== undefined)
116
+ includeEntry.take = selector.take;
117
+ if (selector.skip !== undefined)
118
+ includeEntry.skip = selector.skip;
119
+ if (nestedInclude)
120
+ includeEntry.include = nestedInclude;
121
+ include[key] = Object.keys(includeEntry).length > 0 ? includeEntry : true;
122
+ continue;
123
+ }
124
+ }
125
+ return hasIncludes ? include : undefined;
126
+ }
127
+ /**
128
+ * Recursively pick only the fields requested by a fragment from a raw Prisma
129
+ * result object. This ensures the runtime shape exactly matches the type
130
+ * produced by `ResultOf<F>`.
131
+ *
132
+ * Exported for use in `context/index.ts`.
133
+ * @internal
134
+ */
135
+ export function pickFields(item, fields) {
136
+ const result = {};
137
+ for (const [key, value] of Object.entries(fields)) {
138
+ const fieldValue = item[key];
139
+ if (value === true) {
140
+ result[key] = fieldValue;
141
+ continue;
142
+ }
143
+ if (value === null || typeof value !== 'object')
144
+ continue;
145
+ const val = value;
146
+ // ── Shorthand: Fragment directly ──────────────────────────
147
+ if (isFragment(val)) {
148
+ if (Array.isArray(fieldValue)) {
149
+ result[key] = fieldValue.map((elem) => pickFields(elem, val._fields));
150
+ }
151
+ else if (fieldValue === null || fieldValue === undefined) {
152
+ result[key] = fieldValue;
153
+ }
154
+ else {
155
+ result[key] = pickFields(fieldValue, val._fields);
156
+ }
157
+ continue;
158
+ }
159
+ // ── RelationSelector: { query, where?, ... } ──────────────
160
+ if ('query' in val && isFragment(val.query)) {
161
+ const nestedFrag = val.query;
162
+ if (Array.isArray(fieldValue)) {
163
+ result[key] = fieldValue.map((elem) => pickFields(elem, nestedFrag._fields));
164
+ }
165
+ else if (fieldValue === null || fieldValue === undefined) {
166
+ result[key] = fieldValue;
167
+ }
168
+ else {
169
+ result[key] = pickFields(fieldValue, nestedFrag._fields);
170
+ }
171
+ continue;
172
+ }
173
+ }
174
+ return result;
175
+ }
176
+ // ─────────────────────────────────────────────────────────────
177
+ // Standalone query runners
178
+ // ─────────────────────────────────────────────────────────────
179
+ /**
180
+ * Execute a fragment-based query against a list, returning all matching
181
+ * records shaped to the fragment's field selection.
182
+ *
183
+ * Under the hood this calls `context.db[listKey].findMany()`, so all access
184
+ * control rules defined in your config are still enforced.
185
+ *
186
+ * **Tip:** You can also call `context.db.post.findMany({ query: fragment, ... })`
187
+ * directly — both forms produce the same result.
188
+ *
189
+ * @param context - An `AccessContext` (or any object with a compatible `db`).
190
+ * @param listKey - The PascalCase list name (e.g. `'Post'`, `'BlogPost'`).
191
+ * @param fragment - A fragment created with {@link defineFragment}.
192
+ * @param args - Optional query arguments (where, orderBy, take, skip).
193
+ * @returns An array typed to exactly the fragment's field selection.
194
+ *
195
+ * @example
196
+ * ```ts
197
+ * const posts = await runQuery(context, 'Post', postFragment, {
198
+ * where: { published: true },
199
+ * orderBy: { createdAt: 'desc' },
200
+ * take: 10,
201
+ * })
202
+ * // posts: Array<ResultOf<typeof postFragment>>
203
+ * ```
204
+ */
205
+ export async function runQuery(context, listKey, fragment, args) {
206
+ const dbKey = getDbKey(listKey);
207
+ const include = buildInclude(fragment._fields);
208
+ const findManyArgs = {};
209
+ if (args?.where !== undefined)
210
+ findManyArgs.where = args.where;
211
+ if (args?.orderBy !== undefined)
212
+ findManyArgs.orderBy = args.orderBy;
213
+ if (args?.take !== undefined)
214
+ findManyArgs.take = args.take;
215
+ if (args?.skip !== undefined)
216
+ findManyArgs.skip = args.skip;
217
+ if (include)
218
+ findManyArgs.include = include;
219
+ const results = await context.db[dbKey].findMany(Object.keys(findManyArgs).length > 0 ? findManyArgs : undefined);
220
+ return results.map((item) => pickFields(item, fragment._fields));
221
+ }
222
+ /**
223
+ * Execute a fragment-based query that returns a single record (or `null`).
224
+ *
225
+ * Under the hood this calls `context.db[listKey].findFirst()`, so all access
226
+ * control rules are still enforced.
227
+ *
228
+ * **Tip:** You can also call `context.db.post.findUnique({ where: { id }, query: fragment })`
229
+ * directly.
230
+ *
231
+ * @param context - An `AccessContext` (or any object with a compatible `db`).
232
+ * @param listKey - The PascalCase list name (e.g. `'Post'`).
233
+ * @param fragment - A fragment created with {@link defineFragment}.
234
+ * @param where - A Prisma where clause to identify the record.
235
+ * @returns The matched record shaped to the fragment, or `null`.
236
+ *
237
+ * @example
238
+ * ```ts
239
+ * const post = await runQueryOne(context, 'Post', postFragment, { id: postId })
240
+ * if (!post) return notFound()
241
+ * // post: ResultOf<typeof postFragment>
242
+ * ```
243
+ */
244
+ export async function runQueryOne(context, listKey, fragment, where) {
245
+ const dbKey = getDbKey(listKey);
246
+ const include = buildInclude(fragment._fields);
247
+ const findFirstArgs = { where };
248
+ if (include)
249
+ findFirstArgs.include = include;
250
+ const item = await context.db[dbKey].findFirst(findFirstArgs);
251
+ if (item === null || item === undefined)
252
+ return null;
253
+ return pickFields(item, fragment._fields);
254
+ }
255
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/query/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/query/index.test.ts"],"names":[],"mappings":""}