appflare 0.2.40 → 0.2.42
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/Documentation.md +108 -4
- package/cli/schema-compiler.ts +2 -0
- package/cli/templates/dashboard/builders/functions/tree-builder.ts +47 -0
- package/cli/templates/dashboard/builders/navigation.ts +55 -22
- package/cli/templates/dashboard/components/layout.ts +33 -1
- package/cli/templates/handlers/generators/types/query-definitions/filter-and-where-types.ts +22 -2
- package/cli/templates/handlers/generators/types/query-runtime/runtime-write.ts +275 -14
- package/dist/cli/index.js +700 -375
- package/dist/cli/index.mjs +700 -375
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/schema.ts +1 -1
package/Documentation.md
CHANGED
|
@@ -164,6 +164,7 @@ export const listPostsPage = query({
|
|
|
164
164
|
},
|
|
165
165
|
}
|
|
166
166
|
: {},
|
|
167
|
+
orderBy: { column: "id", direction: "asc" },
|
|
167
168
|
limit: args.pageSize,
|
|
168
169
|
with: {
|
|
169
170
|
owner: true,
|
|
@@ -250,8 +251,8 @@ export const nearbyPlaygroundItems = query({
|
|
|
250
251
|
},
|
|
251
252
|
latitudeField: "latitude",
|
|
252
253
|
longitudeField: "longitude",
|
|
253
|
-
|
|
254
|
-
|
|
254
|
+
gte: 0,
|
|
255
|
+
lt: args.radiusMeters,
|
|
255
256
|
},
|
|
256
257
|
isActive: {
|
|
257
258
|
eq: true,
|
|
@@ -268,7 +269,69 @@ export const nearbyPlaygroundItems = query({
|
|
|
268
269
|
});
|
|
269
270
|
```
|
|
270
271
|
|
|
271
|
-
####
|
|
272
|
+
#### F) Query with `orderBy`
|
|
273
|
+
|
|
274
|
+
The `orderBy` field accepts an object or array of objects with `column` and optional `direction`:
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import { query } from "../../_generated/handlers";
|
|
278
|
+
import * as z from "zod";
|
|
279
|
+
|
|
280
|
+
export const getTopUsers = query({
|
|
281
|
+
args: {
|
|
282
|
+
minScore: z.number().optional(),
|
|
283
|
+
limit: z.number().int().min(1).max(100).default(10),
|
|
284
|
+
},
|
|
285
|
+
handler: async (ctx, args) => {
|
|
286
|
+
return ctx.db.users.findMany({
|
|
287
|
+
where: args.minScore ? { score: { gte: args.minScore } } : {},
|
|
288
|
+
orderBy: { column: "score", direction: "desc" },
|
|
289
|
+
limit: args.limit,
|
|
290
|
+
});
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Multiple sort keys are supported with an array:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
orderBy: [
|
|
299
|
+
{ column: "score", direction: "desc" },
|
|
300
|
+
{ column: "name", direction: "asc" },
|
|
301
|
+
],
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
#### G) Array column operators (`includes`, `includesAny`, `length`)
|
|
305
|
+
|
|
306
|
+
For JSON array columns, use array-specific operators:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
import { query } from "../../_generated/handlers";
|
|
310
|
+
import * as z from "zod";
|
|
311
|
+
|
|
312
|
+
export const findProducts = query({
|
|
313
|
+
args: {
|
|
314
|
+
color: z.string().optional(),
|
|
315
|
+
tags: z.array(z.string()).optional(),
|
|
316
|
+
minTagCount: z.number().int().optional(),
|
|
317
|
+
},
|
|
318
|
+
handler: async (ctx, args) => {
|
|
319
|
+
return ctx.db.products.findMany({
|
|
320
|
+
where: {
|
|
321
|
+
...(args.tags ? { tags: { includes: args.tags } } : {}),
|
|
322
|
+
...(args.minTagCount ? { tags: { length: args.minTagCount } } : {}),
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
- `includes` — row's array must contain **all** specified values
|
|
330
|
+
- `includesAny` — row's array must contain **at least one** of the specified values
|
|
331
|
+
- `length` — matches the array length exactly
|
|
332
|
+
- `eq` / `ne` — exact match on the whole JSON array
|
|
333
|
+
|
|
334
|
+
#### H) Complex production-style query (similar to `db-features`)
|
|
272
335
|
|
|
273
336
|
```ts
|
|
274
337
|
import { query } from "../../_generated/handlers";
|
|
@@ -326,8 +389,11 @@ export const queryDashboardData = query({
|
|
|
326
389
|
- Start with one root query and compose aggregates/relations progressively.
|
|
327
390
|
- Prefer server-side filtering in `where` instead of filtering on frontend.
|
|
328
391
|
- For heavy queries, add `limit`, cursor args, and response metadata (`nextCursor`, `hasMore`).
|
|
392
|
+
- Use `orderBy` with cursor-based pagination to ensure deterministic ordering across pages.
|
|
329
393
|
|
|
330
|
-
### 3.2 Mutation handler
|
|
394
|
+
### 3.2 Mutation handler examples
|
|
395
|
+
|
|
396
|
+
#### Insert
|
|
331
397
|
|
|
332
398
|
```ts
|
|
333
399
|
import { mutation } from "../../_generated/handlers";
|
|
@@ -352,6 +418,37 @@ export const createPost = mutation({
|
|
|
352
418
|
});
|
|
353
419
|
```
|
|
354
420
|
|
|
421
|
+
#### Upsert
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
import { mutation } from "../../_generated/handlers";
|
|
425
|
+
import * as z from "zod";
|
|
426
|
+
|
|
427
|
+
export const upsertPost = mutation({
|
|
428
|
+
args: {
|
|
429
|
+
slug: z.string().min(1),
|
|
430
|
+
title: z.string().min(1),
|
|
431
|
+
},
|
|
432
|
+
handler: async (ctx, args) => {
|
|
433
|
+
const result = await ctx.db.posts.upsert({
|
|
434
|
+
values: {
|
|
435
|
+
slug: args.slug,
|
|
436
|
+
title: args.title,
|
|
437
|
+
ownerId: "some-user-id",
|
|
438
|
+
},
|
|
439
|
+
target: "slug",
|
|
440
|
+
set: { title: args.title },
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
return { updated: result.length };
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
- `target` — conflict column(s) to detect existing rows
|
|
449
|
+
- `set` — columns to update on conflict (omit to keep existing values)
|
|
450
|
+
- Supports single or array of values
|
|
451
|
+
|
|
355
452
|
### 3.3 Handler file placement
|
|
356
453
|
|
|
357
454
|
Put handlers under `packages/backend/src` (including nested directories). Example patterns already used in this repo:
|
|
@@ -372,6 +469,13 @@ Inside handlers, you commonly use:
|
|
|
372
469
|
|
|
373
470
|
- `ctx.db.<table>.findMany/findFirst/insert/update/upsert/delete`
|
|
374
471
|
- aggregate helpers like `count` and `avg`
|
|
472
|
+
- `count` supports `where`, `field`, `distinct`, and `with` for filtered relation counts
|
|
473
|
+
- `avg` supports `where`, `field`, `distinct`, and `with` for filtered relation averages
|
|
474
|
+
- `where` supports shorthand operators: `eq`, `ne`, `in`, `nin`, `gt`, `gte`, `lt`, `lte`, `exists`, `regex`, `$options`, `includes`, `includesAny`, `length`
|
|
475
|
+
- `with` supports `_count` and `_avg` for relation-level aggregate results (returned as `XxxAggregate`)
|
|
476
|
+
- `orderBy` accepts `{ column, direction }` or array thereof
|
|
477
|
+
- `geoWithin` for geospatial distance queries (Haversine formula)
|
|
478
|
+
- `upsert` supports `target` (conflict columns) and `set` (on-conflict update columns)
|
|
375
479
|
- `ctx.error(status, message, details)` for typed failures
|
|
376
480
|
|
|
377
481
|
See real examples in:
|
package/cli/schema-compiler.ts
CHANGED
|
@@ -821,6 +821,8 @@ function emitManyToManyRuntimeMetadata(definition: SchemaDefinition): string {
|
|
|
821
821
|
junctionTable: ${quote(relation.junctionTable)},
|
|
822
822
|
sourceField: ${quote(relation.sourceField ?? "")},
|
|
823
823
|
targetField: ${quote(relation.targetField ?? "")},
|
|
824
|
+
referenceField: ${quote(relation.referenceField ?? "id")},
|
|
825
|
+
targetReferenceField: ${quote(relation.targetReferenceField ?? "id")},
|
|
824
826
|
},`,
|
|
825
827
|
);
|
|
826
828
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DiscoveredHandlerOperation } from "../../../../utils/handler-discovery";
|
|
2
|
+
|
|
3
|
+
export type TreeNode = {
|
|
4
|
+
name: string;
|
|
5
|
+
type: "folder";
|
|
6
|
+
children: TreeNode[];
|
|
7
|
+
handlers: DiscoveredHandlerOperation[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function buildFunctionTree(
|
|
11
|
+
handlers: DiscoveredHandlerOperation[],
|
|
12
|
+
): TreeNode[] {
|
|
13
|
+
const root: TreeNode = {
|
|
14
|
+
name: "root",
|
|
15
|
+
type: "folder",
|
|
16
|
+
children: [],
|
|
17
|
+
handlers: [],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
for (const handler of handlers) {
|
|
21
|
+
const segments = handler.clientSegments ?? [handler.exportName];
|
|
22
|
+
let current = root;
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
25
|
+
const segment = segments[i];
|
|
26
|
+
let child = current.children.find(
|
|
27
|
+
(c) => c.name === segment && c.type === "folder",
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (!child) {
|
|
31
|
+
child = {
|
|
32
|
+
name: segment,
|
|
33
|
+
type: "folder",
|
|
34
|
+
children: [],
|
|
35
|
+
handlers: [],
|
|
36
|
+
};
|
|
37
|
+
current.children.push(child);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
current = child;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
current.handlers.push(handler);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return root.children;
|
|
47
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DiscoveredSchema } from "../../../utils/schema-discovery";
|
|
2
2
|
import { DiscoveredHandlerOperation } from "../../../utils/handler-discovery";
|
|
3
3
|
import { TableInfo } from "../types";
|
|
4
|
+
import { buildFunctionTree, TreeNode } from "./functions/tree-builder";
|
|
4
5
|
|
|
5
6
|
export function collectTablesInfo(schema: DiscoveredSchema): TableInfo[] {
|
|
6
7
|
return schema.tables.map((table) => ({
|
|
@@ -33,35 +34,67 @@ export function buildSidebarTableList(tablesInfo: TableInfo[]): string {
|
|
|
33
34
|
\t\t\t\t\t${tableItems}`;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
function renderTreeNode(node: TreeNode, depth: number): string {
|
|
38
|
+
const indent = " ".repeat(depth);
|
|
39
|
+
const hasChildren = node.children.length > 0;
|
|
40
|
+
const hasHandlers = node.handlers.length > 0;
|
|
41
|
+
const isExpandable = hasChildren || hasHandlers;
|
|
42
|
+
|
|
43
|
+
let html = "";
|
|
44
|
+
|
|
45
|
+
if (isExpandable) {
|
|
46
|
+
const folderId = `folder-${node.name.replace(/[^a-zA-Z0-9]/g, "-")}-${depth}`;
|
|
47
|
+
html += `\n${indent}<li data-name="${node.name}" class="folder-item">`;
|
|
48
|
+
html += `\n${indent} <details class="group/folder" open>`;
|
|
49
|
+
html += `\n${indent} <summary class="sidebar-link flex items-center gap-2 px-3 py-2 text-sm rounded-lg w-full cursor-pointer list-none">`;
|
|
50
|
+
html += `\n${indent} <iconify-icon icon="solar:folder-bold-duotone" width="16" height="16" class="opacity-50 shrink-0 transition-transform group-open/folder:rotate-0"></iconify-icon>`;
|
|
51
|
+
html += `\n${indent} <span class="truncate font-medium">${node.name}</span>`;
|
|
52
|
+
html += `\n${indent} </summary>`;
|
|
53
|
+
html += `\n${indent} <ul class="flex flex-col gap-0.5 ml-4 border-l border-base-200 pl-2">`;
|
|
54
|
+
|
|
55
|
+
for (const child of node.children) {
|
|
56
|
+
html += renderTreeNode(child, depth + 2);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const handler of node.handlers) {
|
|
60
|
+
const icon = handler.kind === "query" ? "solar:reorder-linear" : "solar:bolt-linear";
|
|
61
|
+
html += `\n${indent} <li data-name="${handler.exportName}">`;
|
|
62
|
+
html += `\n${indent} <a href="/admin/functions${handler.routePath}" hx-get="/admin/functions${handler.routePath}" hx-target="#main-content" hx-push-url="true" hx-swap="outerHTML" class="sidebar-link flex items-center gap-2 px-3 py-2 text-sm rounded-lg w-full">`;
|
|
63
|
+
html += `\n${indent} <iconify-icon icon="${icon}" width="16" height="16" class="opacity-50 shrink-0"></iconify-icon>`;
|
|
64
|
+
html += `\n${indent} <span class="truncate">${handler.exportName}</span>`;
|
|
65
|
+
html += `\n${indent} </a>`;
|
|
66
|
+
html += `\n${indent} </li>`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
html += `\n${indent} </ul>`;
|
|
70
|
+
html += `\n${indent} </details>`;
|
|
71
|
+
html += `\n${indent}</li>`;
|
|
72
|
+
} else {
|
|
73
|
+
for (const handler of node.handlers) {
|
|
74
|
+
const icon = handler.kind === "query" ? "solar:reorder-linear" : "solar:bolt-linear";
|
|
75
|
+
html += `\n${indent}<li data-name="${handler.exportName}">`;
|
|
76
|
+
html += `\n${indent} <a href="/admin/functions${handler.routePath}" hx-get="/admin/functions${handler.routePath}" hx-target="#main-content" hx-push-url="true" hx-swap="outerHTML" class="sidebar-link flex items-center gap-2 px-3 py-2 text-sm rounded-lg w-full">`;
|
|
77
|
+
html += `\n${indent} <iconify-icon icon="${icon}" width="16" height="16" class="opacity-50 shrink-0"></iconify-icon>`;
|
|
78
|
+
html += `\n${indent} <span class="truncate">${handler.exportName}</span>`;
|
|
79
|
+
html += `\n${indent} </a>`;
|
|
80
|
+
html += `\n${indent}</li>`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return html;
|
|
85
|
+
}
|
|
86
|
+
|
|
36
87
|
export function buildSidebarFunctionList(
|
|
37
88
|
handlers: DiscoveredHandlerOperation[],
|
|
38
89
|
): string {
|
|
39
90
|
const queries = handlers.filter((h) => h.kind === "query");
|
|
40
91
|
const mutations = handlers.filter((h) => h.kind === "mutation");
|
|
41
92
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
(h) => `
|
|
45
|
-
<li data-name="${h.exportName}">
|
|
46
|
-
<a href="/admin/functions${h.routePath}" hx-get="/admin/functions${h.routePath}" hx-target="#main-content" hx-push-url="true" hx-swap="outerHTML" class="sidebar-link flex items-center gap-2 px-3 py-2 text-sm rounded-lg w-full">
|
|
47
|
-
<iconify-icon icon="solar:reorder-linear" width="16" height="16" class="opacity-50 shrink-0"></iconify-icon>
|
|
48
|
-
<span class="truncate">${h.exportName}</span>
|
|
49
|
-
</a>
|
|
50
|
-
</li>`,
|
|
51
|
-
)
|
|
52
|
-
.join("");
|
|
93
|
+
const queryTree = buildFunctionTree(queries);
|
|
94
|
+
const mutationTree = buildFunctionTree(mutations);
|
|
53
95
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
(h) => `
|
|
57
|
-
<li data-name="${h.exportName}">
|
|
58
|
-
<a href="/admin/functions${h.routePath}" hx-get="/admin/functions${h.routePath}" hx-target="#main-content" hx-push-url="true" hx-swap="outerHTML" class="sidebar-link flex items-center gap-2 px-3 py-2 text-sm rounded-lg w-full">
|
|
59
|
-
<iconify-icon icon="solar:bolt-linear" width="16" height="16" class="opacity-50 shrink-0"></iconify-icon>
|
|
60
|
-
<span class="truncate">${h.exportName}</span>
|
|
61
|
-
</a>
|
|
62
|
-
</li>`,
|
|
63
|
-
)
|
|
64
|
-
.join("");
|
|
96
|
+
const queryItems = queryTree.map((node) => renderTreeNode(node, 0)).join("");
|
|
97
|
+
const mutationItems = mutationTree.map((node) => renderTreeNode(node, 0)).join("");
|
|
65
98
|
|
|
66
99
|
return `
|
|
67
100
|
<div id="pane-functions" class="flex flex-col h-full hidden">
|
|
@@ -112,6 +112,23 @@ function Layout(props: { children: any; title: string; hideSidebar?: boolean })
|
|
|
112
112
|
}
|
|
113
113
|
.sidebar-link:hover iconify-icon { opacity: 0.7; }
|
|
114
114
|
|
|
115
|
+
.folder-item { list-style: none; }
|
|
116
|
+
.folder-item > details > summary { list-style: none; }
|
|
117
|
+
.folder-item > details > summary::-webkit-details-marker { display: none; }
|
|
118
|
+
.folder-item > details > summary::before {
|
|
119
|
+
content: '';
|
|
120
|
+
display: inline-block;
|
|
121
|
+
width: 16px;
|
|
122
|
+
height: 16px;
|
|
123
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6z'/%3E%3C/svg%3E");
|
|
124
|
+
background-size: contain;
|
|
125
|
+
transition: transform 0.15s ease;
|
|
126
|
+
opacity: 0.45;
|
|
127
|
+
}
|
|
128
|
+
.folder-item > details[open] > summary::before {
|
|
129
|
+
transform: rotate(90deg);
|
|
130
|
+
}
|
|
131
|
+
|
|
115
132
|
.table th {
|
|
116
133
|
font-weight: 500;
|
|
117
134
|
font-size: 0.78rem;
|
|
@@ -291,9 +308,24 @@ function Layout(props: { children: any; title: string; hideSidebar?: boolean })
|
|
|
291
308
|
|
|
292
309
|
function filterFunctions(query) {
|
|
293
310
|
var q = query.toLowerCase();
|
|
311
|
+
document.querySelectorAll('#pane-functions details').forEach(function(details) {
|
|
312
|
+
details.open = true;
|
|
313
|
+
});
|
|
294
314
|
document.querySelectorAll('#pane-functions li').forEach(function(li) {
|
|
295
315
|
var name = (li.getAttribute('data-name') || li.textContent).toLowerCase();
|
|
296
|
-
|
|
316
|
+
var matches = name.includes(q);
|
|
317
|
+
li.style.display = matches ? '' : 'none';
|
|
318
|
+
if (!matches && li.classList.contains('folder-item')) {
|
|
319
|
+
var childItems = li.querySelectorAll('li');
|
|
320
|
+
childItems.forEach(function(child) {
|
|
321
|
+
var childName = (child.getAttribute('data-name') || child.textContent).toLowerCase();
|
|
322
|
+
if (childName.includes(q)) {
|
|
323
|
+
li.style.display = '';
|
|
324
|
+
var parentDetails = li.closest('details');
|
|
325
|
+
if (parentDetails) parentDetails.open = true;
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
297
329
|
});
|
|
298
330
|
}
|
|
299
331
|
|
|
@@ -46,7 +46,7 @@ type FieldOperators<T, TFieldKey extends string = string> = {
|
|
|
46
46
|
lte?: Comparable<T>;
|
|
47
47
|
exists?: boolean;
|
|
48
48
|
regex?: RegexOperand<T>;
|
|
49
|
-
options?: string;
|
|
49
|
+
$options?: string;
|
|
50
50
|
geoWithin?: GeoWithinOperandForField<TFieldKey>;
|
|
51
51
|
includes?: T extends ReadonlyArray<infer E> ? ReadonlyArray<E> : never;
|
|
52
52
|
includesAny?: T extends ReadonlyArray<infer E> ? ReadonlyArray<E> : never;
|
|
@@ -258,11 +258,31 @@ export type QueryInsertArgs<TName extends TableName> = {
|
|
|
258
258
|
};
|
|
259
259
|
|
|
260
260
|
export type QueryUpdateArgs<TName extends TableName> = {
|
|
261
|
-
set: Partial<TableInsertModel<TName
|
|
261
|
+
set: Partial<TableInsertModel<TName>> & ManyToManyUpdateSetFields<TName>;
|
|
262
262
|
where?: WhereInput<TableModel<TName>, TName>;
|
|
263
263
|
limit?: number;
|
|
264
264
|
};
|
|
265
265
|
|
|
266
|
+
type ManyToManyUpdateSetFields<TName extends TableName> = {
|
|
267
|
+
[TRelationName in RuntimeRelationName<TName>]?: RuntimeRelationKind<TName, TRelationName> extends "manyToMany"
|
|
268
|
+
? ManyToManyUpdateInput<TName, TRelationName>
|
|
269
|
+
: never;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
type ManyToManyUpdateInput<
|
|
273
|
+
TSourceTable extends TableName,
|
|
274
|
+
TRelationName extends RuntimeRelationName<TSourceTable>,
|
|
275
|
+
> = {
|
|
276
|
+
items: Array<ManyToManyUpdateItem<TargetTableForRelation<TSourceTable, TRelationName>>>;
|
|
277
|
+
mode?: "merge" | "overwrite";
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
type ManyToManyUpdateItem<TTargetTable extends TableName> =
|
|
281
|
+
| ("id" extends keyof TableModel<TTargetTable>
|
|
282
|
+
? TableModel<TTargetTable>["id"]
|
|
283
|
+
: never)
|
|
284
|
+
| Partial<TableInsertModel<TTargetTable>>;
|
|
285
|
+
|
|
266
286
|
export type QueryDeleteArgs<TName extends TableName> = {
|
|
267
287
|
where?: WhereInput<TableModel<TName>, TName>;
|
|
268
288
|
limit?: number;
|