appflare 0.0.14 → 0.0.15

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.
@@ -29,6 +29,13 @@ export async function discoverHandlers(params: {
29
29
  if (path.resolve(fileAbs) === path.resolve(params.schemaPathAbs)) continue;
30
30
  if (path.resolve(fileAbs) === path.resolve(params.configPathAbs)) continue;
31
31
 
32
+ const relPathRaw = path.relative(params.projectDirAbs, fileAbs);
33
+ const relPath = relPathRaw.replace(/\\/g, "/");
34
+ const rawRoutePath = relPath.replace(/\.ts$/, "");
35
+ const routePath = rawRoutePath.endsWith("/index")
36
+ ? rawRoutePath.slice(0, -"/index".length) || "index"
37
+ : rawRoutePath;
38
+
32
39
  const content = await fs.readFile(fileAbs, "utf8");
33
40
  const cronTriggersByHandler = extractCronTriggers(content);
34
41
  const regex =
@@ -38,6 +45,7 @@ export async function discoverHandlers(params: {
38
45
  const kind = match[2] as HandlerKind;
39
46
  handlers.push({
40
47
  fileName: path.basename(fileAbs, ".ts"),
48
+ routePath,
41
49
  name: match[1],
42
50
  kind,
43
51
  sourceFileAbs: fileAbs,
@@ -50,7 +58,7 @@ export async function discoverHandlers(params: {
50
58
  // De-dupe: keep first occurrence
51
59
  const seen = new Set<string>();
52
60
  const unique = handlers.filter((h) => {
53
- const key = `${h.kind}:${h.fileName}:${h.name}`;
61
+ const key = `${h.kind}:${h.routePath}:${h.name}`;
54
62
  if (seen.has(key)) return false;
55
63
  seen.add(key);
56
64
  return true;
@@ -58,6 +66,8 @@ export async function discoverHandlers(params: {
58
66
 
59
67
  unique.sort((a, b) => {
60
68
  if (a.kind !== b.kind) return a.kind.localeCompare(b.kind);
69
+ if (a.routePath !== b.routePath)
70
+ return a.routePath.localeCompare(b.routePath);
61
71
  if (a.fileName !== b.fileName) return a.fileName.localeCompare(b.fileName);
62
72
  return a.name.localeCompare(b.name);
63
73
  });
@@ -3,91 +3,161 @@ import {
3
3
  handlerTypePrefix,
4
4
  normalizeTableName,
5
5
  renderObjectKey,
6
- sortedEntries,
7
6
  } from "./utils";
8
7
 
8
+ type PathTree<T> = {
9
+ leaf?: { path: string; items: T[] };
10
+ children: Map<string, PathTree<T>>;
11
+ };
12
+
13
+ const buildPathTree = <T>(byPath: Map<string, T[]>): PathTree<T> => {
14
+ const root: PathTree<T> = { children: new Map() };
15
+ for (const [path, items] of Array.from(byPath.entries())) {
16
+ const segments = path.split("/").filter(Boolean);
17
+ let node = root;
18
+ for (const segment of segments) {
19
+ if (!node.children.has(segment)) {
20
+ node.children.set(segment, { children: new Map() });
21
+ }
22
+ node = node.children.get(segment)!;
23
+ }
24
+ node.leaf = { path, items };
25
+ }
26
+ return root;
27
+ };
28
+
29
+ const indent = (depth: number): string => "\t".repeat(depth);
30
+
31
+ const renderPathTreeLines = <T>(
32
+ node: PathTree<T>,
33
+ depth: number,
34
+ renderLeaf: (leaf: { path: string; items: T[] }, depth: number) => string[]
35
+ ): string[] => {
36
+ const lines: string[] = [];
37
+ if (node.leaf) {
38
+ lines.push(...renderLeaf(node.leaf, depth));
39
+ }
40
+ const children = Array.from(node.children.entries()).sort((a, b) =>
41
+ a[0].localeCompare(b[0])
42
+ );
43
+ for (const [segment, child] of children) {
44
+ lines.push(`${indent(depth + 1)}${renderObjectKey(segment)}: {`);
45
+ lines.push(...renderPathTreeLines(child, depth + 1, renderLeaf));
46
+ lines.push(`${indent(depth + 1)}}`);
47
+ }
48
+ return lines;
49
+ };
50
+
9
51
  export function generateQueriesClientLines(
10
52
  queriesByFile: Map<string, DiscoveredHandler[]>,
11
53
  importAliasBySource: Map<string, string>
12
54
  ): string {
13
- return sortedEntries(queriesByFile)
14
- .map(([fileName, list]) => {
15
- const fileKey = renderObjectKey(fileName);
16
- const inner = list
17
- .slice()
18
- .sort((a, b) => a.name.localeCompare(b.name))
19
- .map((h) => {
20
- const pascal = handlerTypePrefix(h);
21
- const route = `/queries/${fileName}/${h.name}`;
22
- const importAlias = importAliasBySource.get(h.sourceFileAbs)!;
23
- const handlerAccessor = `${importAlias}.${h.name}`;
24
- return (
25
- `\t\t${h.name}: withHandlerMetadata<${pascal}Definition>(\n` +
26
- `\t\t\tasync (args: ${pascal}Args, init) => {\n` +
27
- `\t\t\t\tconst url = buildQueryUrl(baseUrl, ${JSON.stringify(route)}, args);\n` +
28
- `\t\t\t\tconst response = await request(url, {\n` +
29
- `\t\t\t\t\t...(init ?? {}),\n` +
30
- `\t\t\t\t\tmethod: "GET",\n` +
31
- `\t\t\t\t});\n` +
32
- `\t\t\t\treturn parseJson<${pascal}Result>(response);\n` +
33
- `\t\t\t},\n` +
34
- `\t\t\t{\n` +
35
- `\t\t\t\tschema: createHandlerSchema(${handlerAccessor}.args),\n` +
36
- `\t\t\t\twebsocket: createHandlerWebsocket<${pascal}Args, ${pascal}Result>(realtime, {\n` +
37
- `\t\t\t\t\tdefaultTable: ${JSON.stringify(normalizeTableName(fileName))},\n` +
38
- `\t\t\t\t\tdefaultHandler: { file: ${JSON.stringify(fileName)}, name: ${JSON.stringify(h.name)} },\n` +
39
- `\t\t\t\t}),\n` +
40
- `\t\t\t\tpath: ${JSON.stringify(route)},\n` +
41
- `\t\t\t}\n` +
42
- `\t\t),`
43
- );
44
- })
45
- .join("\n");
46
- return `\t${fileKey}: {\n${inner || "\t\t// (none)"}\n\t},`;
47
- })
48
- .join("\n");
55
+ const tree = buildPathTree(queriesByFile);
56
+ const renderLeaf = (
57
+ leaf: { path: string; items: DiscoveredHandler[] },
58
+ depth: number
59
+ ): string[] => {
60
+ const inner = leaf.items
61
+ .slice()
62
+ .sort((a, b) => a.name.localeCompare(b.name))
63
+ .map((h) => {
64
+ const pascal = handlerTypePrefix(h);
65
+ const route = `/queries/${leaf.path}/${h.name}`;
66
+ const importAlias = importAliasBySource.get(h.sourceFileAbs)!;
67
+ const handlerAccessor = `${importAlias}.${h.name}`;
68
+ const pad = indent(depth + 1);
69
+ return (
70
+ `${pad}${h.name}: withHandlerMetadata<${pascal}Definition>(\n` +
71
+ `${pad}\tasync (args: ${pascal}Args, init) => {\n` +
72
+ `${pad}\t\tconst url = buildQueryUrl(baseUrl, ${JSON.stringify(route)}, args);\n` +
73
+ `${pad}\t\tconst response = await request(url, {\n` +
74
+ `${pad}\t\t\t...(init ?? {}),\n` +
75
+ `${pad}\t\t\tmethod: "GET",\n` +
76
+ `${pad}\t\t});\n` +
77
+ `${pad}\t\treturn parseJson<${pascal}Result>(response);\n` +
78
+ `${pad}\t},\n` +
79
+ `${pad}\t{\n` +
80
+ `${pad}\t\tschema: createHandlerSchema(${handlerAccessor}.args),\n` +
81
+ `${pad}\t\twebsocket: createHandlerWebsocket<${pascal}Args, ${pascal}Result>(realtime, {\n` +
82
+ `${pad}\t\t\tdefaultTable: ${JSON.stringify(normalizeTableName(h.fileName))},\n` +
83
+ `${pad}\t\t\tdefaultHandler: { file: ${JSON.stringify(leaf.path)}, name: ${JSON.stringify(h.name)} },\n` +
84
+ `${pad}\t\t}),\n` +
85
+ `${pad}\t\tpath: ${JSON.stringify(route)},\n` +
86
+ `${pad}\t}\n` +
87
+ `${pad}),`
88
+ );
89
+ })
90
+ .join("\n");
91
+
92
+ return inner ? [inner] : [`${indent(depth + 1)}// (none)`];
93
+ };
94
+
95
+ const lines: string[] = [];
96
+ const children = Array.from(tree.children.entries()).sort((a, b) =>
97
+ a[0].localeCompare(b[0])
98
+ );
99
+ for (const [segment, child] of children) {
100
+ lines.push(`${indent(1)}${renderObjectKey(segment)}: {`);
101
+ lines.push(...renderPathTreeLines(child, 1, renderLeaf));
102
+ lines.push(`${indent(1)}}`);
103
+ }
104
+ return lines.join("\n");
49
105
  }
50
106
 
51
107
  export function generateMutationsClientLines(
52
108
  mutationsByFile: Map<string, DiscoveredHandler[]>,
53
109
  importAliasBySource: Map<string, string>
54
110
  ): string {
55
- return sortedEntries(mutationsByFile)
56
- .map(([fileName, list]) => {
57
- const fileKey = renderObjectKey(fileName);
58
- const inner = list
59
- .slice()
60
- .sort((a, b) => a.name.localeCompare(b.name))
61
- .map((h) => {
62
- const pascal = handlerTypePrefix(h);
63
- const route = `/mutations/${fileName}/${h.name}`;
64
- const importAlias = importAliasBySource.get(h.sourceFileAbs)!;
65
- const handlerAccessor = `${importAlias}.${h.name}`;
66
- return (
67
- `\t\t${h.name}: withHandlerMetadata<${pascal}Definition>(\n` +
68
- `\t\t\tasync (args: ${pascal}Args, init) => {\n` +
69
- `\t\t\t\tconst url = buildUrl(baseUrl, ${JSON.stringify(route)});\n` +
70
- `\t\t\t\tconst response = await request(url, {\n` +
71
- `\t\t\t\t\t...(init ?? {}),\n` +
72
- `\t\t\t\t\tmethod: "POST",\n` +
73
- `\t\t\t\t\theaders: ensureJsonHeaders(init?.headers),\n` +
74
- `\t\t\t\t\tbody: JSON.stringify(args),\n` +
75
- `\t\t\t\t});\n` +
76
- `\t\t\t\treturn parseJson<${pascal}Result>(response);\n` +
77
- `\t\t\t},\n` +
78
- `\t\t\t{\n` +
79
- `\t\t\t\tschema: createHandlerSchema(${handlerAccessor}.args),\n` +
80
- `\t\t\t\twebsocket: createHandlerWebsocket<${pascal}Args, ${pascal}Result>(realtime, {\n` +
81
- `\t\t\t\t\tdefaultTable: ${JSON.stringify(normalizeTableName(fileName))},\n` +
82
- `\t\t\t\t\tdefaultHandler: { file: ${JSON.stringify(fileName)}, name: ${JSON.stringify(h.name)} },\n` +
83
- `\t\t\t\t}),\n` +
84
- `\t\t\t\tpath: ${JSON.stringify(route)},\n` +
85
- `\t\t\t}\n` +
86
- `\t\t),`
87
- );
88
- })
89
- .join("\n");
90
- return `\t${fileKey}: {\n${inner || "\t\t// (none)"}\n\t},`;
91
- })
92
- .join("\n");
111
+ const tree = buildPathTree(mutationsByFile);
112
+ const renderLeaf = (
113
+ leaf: { path: string; items: DiscoveredHandler[] },
114
+ depth: number
115
+ ): string[] => {
116
+ const inner = leaf.items
117
+ .slice()
118
+ .sort((a, b) => a.name.localeCompare(b.name))
119
+ .map((h) => {
120
+ const pascal = handlerTypePrefix(h);
121
+ const route = `/mutations/${leaf.path}/${h.name}`;
122
+ const importAlias = importAliasBySource.get(h.sourceFileAbs)!;
123
+ const handlerAccessor = `${importAlias}.${h.name}`;
124
+ const pad = indent(depth + 1);
125
+ return (
126
+ `${pad}${h.name}: withHandlerMetadata<${pascal}Definition>(\n` +
127
+ `${pad}\tasync (args: ${pascal}Args, init) => {\n` +
128
+ `${pad}\t\tconst url = buildUrl(baseUrl, ${JSON.stringify(route)});\n` +
129
+ `${pad}\t\tconst response = await request(url, {\n` +
130
+ `${pad}\t\t\t...(init ?? {}),\n` +
131
+ `${pad}\t\t\tmethod: "POST",\n` +
132
+ `${pad}\t\t\theaders: ensureJsonHeaders(init?.headers),\n` +
133
+ `${pad}\t\t\tbody: JSON.stringify(args),\n` +
134
+ `${pad}\t\t});\n` +
135
+ `${pad}\t\treturn parseJson<${pascal}Result>(response);\n` +
136
+ `${pad}\t},\n` +
137
+ `${pad}\t{\n` +
138
+ `${pad}\t\tschema: createHandlerSchema(${handlerAccessor}.args),\n` +
139
+ `${pad}\t\twebsocket: createHandlerWebsocket<${pascal}Args, ${pascal}Result>(realtime, {\n` +
140
+ `${pad}\t\t\tdefaultTable: ${JSON.stringify(normalizeTableName(h.fileName))},\n` +
141
+ `${pad}\t\t\tdefaultHandler: { file: ${JSON.stringify(leaf.path)}, name: ${JSON.stringify(h.name)} },\n` +
142
+ `${pad}\t\t}),\n` +
143
+ `${pad}\t\tpath: ${JSON.stringify(route)},\n` +
144
+ `${pad}\t}\n` +
145
+ `${pad}),`
146
+ );
147
+ })
148
+ .join("\n");
149
+
150
+ return inner ? [inner] : [`${indent(depth + 1)}// (none)`];
151
+ };
152
+
153
+ const lines: string[] = [];
154
+ const children = Array.from(tree.children.entries()).sort((a, b) =>
155
+ a[0].localeCompare(b[0])
156
+ );
157
+ for (const [segment, child] of children) {
158
+ lines.push(`${indent(1)}${renderObjectKey(segment)}: {`);
159
+ lines.push(...renderPathTreeLines(child, 1, renderLeaf));
160
+ lines.push(`${indent(1)}}`);
161
+ }
162
+ return lines.join("\n");
93
163
  }
@@ -681,7 +681,7 @@ function generateImports(params: {
681
681
  const importLines: string[] = [];
682
682
  const importAliasBySource = new Map<string, string>();
683
683
  for (const [fileAbs, list] of Array.from(handlerImportsGrouped.entries())) {
684
- const alias = `__appflare_${pascalCase(list[0].fileName)}`;
684
+ const alias = `__appflare_${pascalCase(list[0].routePath)}`;
685
685
  importAliasBySource.set(fileAbs, alias);
686
686
  const importPath = toImportPathFromGeneratedSrc(params.outDirAbs, fileAbs);
687
687
  importLines.push(
@@ -704,10 +704,13 @@ function generateGroupedHandlers(handlers: DiscoveredHandler[]): {
704
704
  (h) => h.kind === "internalMutation"
705
705
  );
706
706
 
707
- const queriesByFile = groupBy(queries, (h) => h.fileName);
708
- const mutationsByFile = groupBy(mutations, (h) => h.fileName);
709
- const internalQueriesByFile = groupBy(internalQueries, (h) => h.fileName);
710
- const internalMutationsByFile = groupBy(internalMutations, (h) => h.fileName);
707
+ const queriesByFile = groupBy(queries, (h) => h.routePath);
708
+ const mutationsByFile = groupBy(mutations, (h) => h.routePath);
709
+ const internalQueriesByFile = groupBy(internalQueries, (h) => h.routePath);
710
+ const internalMutationsByFile = groupBy(
711
+ internalMutations,
712
+ (h) => h.routePath
713
+ );
711
714
 
712
715
  return {
713
716
  queriesByFile,
@@ -1,5 +1,48 @@
1
1
  import { DiscoveredHandler } from "../../utils/utils";
2
- import { handlerTypePrefix, renderObjectKey, sortedEntries } from "./utils";
2
+ import { handlerTypePrefix, renderObjectKey } from "./utils";
3
+
4
+ type PathTree<T> = {
5
+ leaf?: { path: string; items: T[] };
6
+ children: Map<string, PathTree<T>>;
7
+ };
8
+
9
+ const buildPathTree = <T>(byPath: Map<string, T[]>): PathTree<T> => {
10
+ const root: PathTree<T> = { children: new Map() };
11
+ for (const [path, items] of Array.from(byPath.entries())) {
12
+ const segments = path.split("/").filter(Boolean);
13
+ let node = root;
14
+ for (const segment of segments) {
15
+ if (!node.children.has(segment)) {
16
+ node.children.set(segment, { children: new Map() });
17
+ }
18
+ node = node.children.get(segment)!;
19
+ }
20
+ node.leaf = { path, items };
21
+ }
22
+ return root;
23
+ };
24
+
25
+ const indent = (depth: number): string => "\t".repeat(depth);
26
+
27
+ const renderPathTreeLines = <T>(
28
+ node: PathTree<T>,
29
+ depth: number,
30
+ renderLeaf: (leaf: { path: string; items: T[] }, depth: number) => string[]
31
+ ): string[] => {
32
+ const lines: string[] = [];
33
+ if (node.leaf) {
34
+ lines.push(...renderLeaf(node.leaf, depth));
35
+ }
36
+ const children = Array.from(node.children.entries()).sort((a, b) =>
37
+ a[0].localeCompare(b[0])
38
+ );
39
+ for (const [segment, child] of children) {
40
+ lines.push(`${indent(depth + 1)}${renderObjectKey(segment)}: {`);
41
+ lines.push(...renderPathTreeLines(child, depth + 1, renderLeaf));
42
+ lines.push(`${indent(depth + 1)}}`);
43
+ }
44
+ return lines;
45
+ };
3
46
 
4
47
  export function generateTypeBlocks(
5
48
  handlers: DiscoveredHandler[],
@@ -23,57 +66,99 @@ export function generateTypeBlocks(
23
66
  export function generateQueriesTypeLines(
24
67
  queriesByFile: Map<string, DiscoveredHandler[]>
25
68
  ): string {
26
- return sortedEntries(queriesByFile)
27
- .map(([fileName, list]) => {
28
- const fileKey = renderObjectKey(fileName);
29
- const inner = list
30
- .slice()
31
- .sort((a, b) => a.name.localeCompare(b.name))
32
- .map((h) => {
33
- const pascal = handlerTypePrefix(h);
34
- return `\t\t${h.name}: ${pascal}Client;`;
35
- })
36
- .join("\n");
37
- return `\t${fileKey}: {\n${inner || "\t\t// (none)"}\n\t};`;
38
- })
39
- .join("\n");
69
+ const tree = buildPathTree(queriesByFile);
70
+ const renderLeaf = (
71
+ leaf: { path: string; items: DiscoveredHandler[] },
72
+ depth: number
73
+ ): string[] => {
74
+ const inner = leaf.items
75
+ .slice()
76
+ .sort((a, b) => a.name.localeCompare(b.name))
77
+ .map((h) => {
78
+ const pascal = handlerTypePrefix(h);
79
+ return `${indent(depth + 1)}${h.name}: ${pascal}Client;`;
80
+ })
81
+ .join("\n");
82
+
83
+ return inner ? [inner] : [`${indent(depth + 1)}// (none)`];
84
+ };
85
+
86
+ const lines: string[] = [];
87
+ const children = Array.from(tree.children.entries()).sort((a, b) =>
88
+ a[0].localeCompare(b[0])
89
+ );
90
+ for (const [segment, child] of children) {
91
+ lines.push(`${indent(1)}${renderObjectKey(segment)}: {`);
92
+ lines.push(...renderPathTreeLines(child, 1, renderLeaf));
93
+ lines.push(`${indent(1)}}`);
94
+ }
95
+
96
+ return lines.join("\n");
40
97
  }
41
98
 
42
99
  export function generateMutationsTypeLines(
43
100
  mutationsByFile: Map<string, DiscoveredHandler[]>
44
101
  ): string {
45
- return sortedEntries(mutationsByFile)
46
- .map(([fileName, list]) => {
47
- const fileKey = renderObjectKey(fileName);
48
- const inner = list
49
- .slice()
50
- .sort((a, b) => a.name.localeCompare(b.name))
51
- .map((h) => {
52
- const pascal = handlerTypePrefix(h);
53
- return `\t\t${h.name}: ${pascal}Client;`;
54
- })
55
- .join("\n");
56
- return `\t${fileKey}: {\n${inner || "\t\t// (none)"}\n\t};`;
57
- })
58
- .join("\n");
102
+ const tree = buildPathTree(mutationsByFile);
103
+ const renderLeaf = (
104
+ leaf: { path: string; items: DiscoveredHandler[] },
105
+ depth: number
106
+ ): string[] => {
107
+ const inner = leaf.items
108
+ .slice()
109
+ .sort((a, b) => a.name.localeCompare(b.name))
110
+ .map((h) => {
111
+ const pascal = handlerTypePrefix(h);
112
+ return `${indent(depth + 1)}${h.name}: ${pascal}Client;`;
113
+ })
114
+ .join("\n");
115
+
116
+ return inner ? [inner] : [`${indent(depth + 1)}// (none)`];
117
+ };
118
+
119
+ const lines: string[] = [];
120
+ const children = Array.from(tree.children.entries()).sort((a, b) =>
121
+ a[0].localeCompare(b[0])
122
+ );
123
+ for (const [segment, child] of children) {
124
+ lines.push(`${indent(1)}${renderObjectKey(segment)}: {`);
125
+ lines.push(...renderPathTreeLines(child, 1, renderLeaf));
126
+ lines.push(`${indent(1)}}`);
127
+ }
128
+
129
+ return lines.join("\n");
59
130
  }
60
131
 
61
132
  export function generateInternalTypeLines(
62
133
  internalByFile: Map<string, DiscoveredHandler[]>,
63
134
  importAliasBySource: Map<string, string>
64
135
  ): string {
65
- return sortedEntries(internalByFile)
66
- .map(([fileName, list]) => {
67
- const fileKey = renderObjectKey(fileName);
68
- const inner = list
69
- .slice()
70
- .sort((a, b) => a.name.localeCompare(b.name))
71
- .map((h) => {
72
- const alias = importAliasBySource.get(h.sourceFileAbs)!;
73
- return `\t\t${h.name}: typeof ${alias}[${JSON.stringify(h.name)}];`;
74
- })
75
- .join("\n");
76
- return `\t${fileKey}: {\n${inner || "\t\t// (none)"}\n\t};`;
77
- })
78
- .join("\n");
136
+ const tree = buildPathTree(internalByFile);
137
+ const renderLeaf = (
138
+ leaf: { path: string; items: DiscoveredHandler[] },
139
+ depth: number
140
+ ): string[] => {
141
+ const inner = leaf.items
142
+ .slice()
143
+ .sort((a, b) => a.name.localeCompare(b.name))
144
+ .map((h) => {
145
+ const alias = importAliasBySource.get(h.sourceFileAbs)!;
146
+ return `${indent(depth + 1)}${h.name}: typeof ${alias}[${JSON.stringify(h.name)}];`;
147
+ })
148
+ .join("\n");
149
+
150
+ return inner ? [inner] : [`${indent(depth + 1)}// (none)`];
151
+ };
152
+
153
+ const lines: string[] = [];
154
+ const children = Array.from(tree.children.entries()).sort((a, b) =>
155
+ a[0].localeCompare(b[0])
156
+ );
157
+ for (const [segment, child] of children) {
158
+ lines.push(`${indent(1)}${renderObjectKey(segment)}: {`);
159
+ lines.push(...renderPathTreeLines(child, 1, renderLeaf));
160
+ lines.push(`${indent(1)}}`);
161
+ }
162
+
163
+ return lines.join("\n");
79
164
  }
@@ -6,13 +6,17 @@ export const sortedEntries = <T>(map: Map<string, T[]>): Array<[string, T[]]> =>
6
6
  export const renderObjectKey = (key: string): string =>
7
7
  isValidIdentifier(key) ? key : JSON.stringify(key);
8
8
 
9
- export const handlerTypePrefix = (h: any): string =>
10
- h.fileName
11
- .replace(/[^a-zA-Z0-9]/g, "")
12
- .replace(/^./, (c: string) => c.toUpperCase()) +
13
- h.name
14
- .replace(/[^a-zA-Z0-9]/g, "")
15
- .replace(/^./, (c: string) => c.toUpperCase());
9
+ export const handlerTypePrefix = (h: any): string => {
10
+ const base = (h.routePath ?? h.fileName) as string;
11
+ return (
12
+ base
13
+ .replace(/[^a-zA-Z0-9]/g, "")
14
+ .replace(/^./, (c: string) => c.toUpperCase()) +
15
+ (h.name as string)
16
+ .replace(/[^a-zA-Z0-9]/g, "")
17
+ .replace(/^./, (c: string) => c.toUpperCase())
18
+ );
19
+ };
16
20
 
17
21
  export const normalizeTableName = (fileName: string): string =>
18
22
  fileName.endsWith("s") ? fileName : `${fileName}s`;
@@ -14,11 +14,11 @@ export const buildCronHandlerEntries = (params: {
14
14
  return params.handlers
15
15
  .map((handler) => {
16
16
  const local = params.localNameFor(handler);
17
- const task = `${handler.fileName}/${handler.name}`;
17
+ const task = `${handler.routePath}/${handler.name}`;
18
18
  const fallbackTriggers = stringifyTriggers(handler.cronTriggers);
19
19
  return (
20
20
  `\t${JSON.stringify(task)}: {\n` +
21
- `\t\tfile: ${JSON.stringify(handler.fileName)},\n` +
21
+ `\t\tfile: ${JSON.stringify(handler.routePath)},\n` +
22
22
  `\t\tname: ${JSON.stringify(handler.name)},\n` +
23
23
  `\t\tcronTrigger: ${local}.cronTrigger ?? ${fallbackTriggers},\n` +
24
24
  `\t\trun: ${local}.handler,\n` +
@@ -30,7 +30,7 @@ export function buildImportSection(params: {
30
30
  );
31
31
  const configImportLine = `import appflareConfig from ${JSON.stringify(configImportPath)};`;
32
32
  const localNameFor = (handler: DiscoveredHandler): string =>
33
- `__appflare_${pascalCase(handler.fileName)}_${handler.name}`;
33
+ `__appflare_${pascalCase(handler.routePath)}_${handler.name}`;
34
34
  const grouped = groupBy(params.handlers, (handler) => handler.sourceFileAbs);
35
35
  const handlerImports: string[] = [];
36
36
  for (const [fileAbs, list] of Array.from(grouped.entries())) {
@@ -10,7 +10,7 @@ export function buildRouteLines(params: {
10
10
  const local = params.localNameFor(query);
11
11
  routeLines.push(
12
12
  `app.get(\n` +
13
- `\t${JSON.stringify(`/queries/${query.fileName}/${query.name}`)},\n` +
13
+ ` ${JSON.stringify(`/queries/${query.routePath}/${query.name}`)},\n` +
14
14
  `\tsValidator("query", z.object(${local}.args as any)),\n` +
15
15
  `\tasync (c) => {\n` +
16
16
  `\t\ttry {\n` +
@@ -39,7 +39,7 @@ export function buildRouteLines(params: {
39
39
  const local = params.localNameFor(mutation);
40
40
  routeLines.push(
41
41
  `app.post(\n` +
42
- `\t${JSON.stringify(`/mutations/${mutation.fileName}/${mutation.name}`)},\n` +
42
+ `\t${JSON.stringify(`/mutations/${mutation.routePath}/${mutation.name}`)},\n` +
43
43
  `\tsValidator("json", z.object(${local}.args as any)),\n` +
44
44
  `\tasync (c) => {\n` +
45
45
  `\t\ttry {\n` +
@@ -58,7 +58,7 @@ export function buildRouteLines(params: {
58
58
  `\t\t\t\ttry {\n` +
59
59
  `\t\t\t\t\tawait notifyMutation({\n` +
60
60
  `\t\t\t\t\t table: normalizeTableName(${JSON.stringify(mutation.fileName)}),\n` +
61
- `\t\t\t\t\t handler: { file: ${JSON.stringify(mutation.fileName)}, name: ${JSON.stringify(mutation.name)} },\n` +
61
+ `\t\t\t\t\t handler: { file: ${JSON.stringify(mutation.routePath)}, name: ${JSON.stringify(mutation.name)} },\n` +
62
62
  `\t\t\t\t\t args: body,\n` +
63
63
  `\t\t\t\t\t result,\n` +
64
64
  `\t\t\t\t});\n` +
@@ -9,10 +9,10 @@ export const buildHandlerEntries = (params: {
9
9
  return params.handlers
10
10
  .map((handler) => {
11
11
  const local = params.localNameFor(handler);
12
- const task = `${handler.fileName}/${handler.name}`;
12
+ const task = `${handler.routePath}/${handler.name}`;
13
13
  return (
14
14
  `\t${JSON.stringify(task)}: {\n` +
15
- `\t\tfile: ${JSON.stringify(handler.fileName)},\n` +
15
+ ` \tfile: ${JSON.stringify(handler.routePath)},\n` +
16
16
  `\t\tname: ${JSON.stringify(handler.name)},\n` +
17
17
  `\t\trun: ${local}.handler,\n` +
18
18
  `\t},`
@@ -28,7 +28,7 @@ export function buildImportSection(params: {
28
28
  );
29
29
 
30
30
  const localNameFor = (handler: DiscoveredHandler): string =>
31
- `__appflare_${pascalCase(handler.fileName)}_${handler.name}`;
31
+ `__appflare_${pascalCase(handler.routePath)}_${handler.name}`;
32
32
 
33
33
  const grouped = groupBy(params.queries, (handler) => handler.sourceFileAbs);
34
34
  const handlerImports: string[] = [];
@@ -7,12 +7,12 @@ export function buildQueryHandlerEntries(params: {
7
7
  return params.queries
8
8
  .slice()
9
9
  .sort((a, b) => {
10
- if (a.fileName === b.fileName) return a.name.localeCompare(b.name);
11
- return a.fileName.localeCompare(b.fileName);
10
+ if (a.routePath === b.routePath) return a.name.localeCompare(b.name);
11
+ return a.routePath.localeCompare(b.routePath);
12
12
  })
13
13
  .map(
14
14
  (query) =>
15
- `\t${JSON.stringify(`${query.fileName}/${query.name}`)}: { file: ${JSON.stringify(query.fileName)}, name: ${JSON.stringify(query.name)}, definition: ${params.localNameFor(query)} },`
15
+ ` ${JSON.stringify(`${query.routePath}/${query.name}`)}: { file: ${JSON.stringify(query.routePath)}, name: ${JSON.stringify(query.name)}, definition: ${params.localNameFor(query)} },`
16
16
  )
17
17
  .join("\n");
18
18
  }
@@ -137,10 +137,17 @@ const defaultHandlerForTable = (
137
137
  \t\tpossible.push(tableStr.slice(0, -1));
138
138
  \t}
139
139
  \tfor (const candidate of possible) {
140
- \t\tconst key = candidate + "/get" + pascalCase(candidate);
141
- \t\tif (key in queryHandlers) {
142
- \t\t\treturn { file: candidate, name: "get" + pascalCase(candidate) };
143
- \t\t}
140
+ const handlerName = "get" + pascalCase(candidate);
141
+ const suffix = "/" + handlerName;
142
+ const matchKey = Object.keys(queryHandlers).find((key) => {
143
+ if (!key.endsWith(suffix)) return false;
144
+ const segments = key.split("/");
145
+ return segments.length >= 2 && segments[segments.length - 2] === candidate;
146
+ });
147
+ if (matchKey) {
148
+ const file = matchKey.slice(0, matchKey.length - suffix.length);
149
+ return { file, name: handlerName };
150
+ }
144
151
  \t}
145
152
  \treturn null;
146
153
  };
@@ -52,6 +52,8 @@ export type HandlerKind =
52
52
 
53
53
  export type DiscoveredHandler = {
54
54
  fileName: string;
55
+ /** Path relative to project root without .ts extension, using forward slashes. */
56
+ routePath: string;
55
57
  name: string;
56
58
  kind: HandlerKind;
57
59
  sourceFileAbs: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appflare",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "bin": {
5
5
  "appflare": "./cli/index.ts"
6
6
  },