@cedarjs/cli 3.0.0-canary.13429 → 3.0.0-canary.13430
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/dist/commands/setup/live-queries/liveQueries.js +31 -0
- package/dist/commands/setup/live-queries/liveQueriesHandler.js +282 -0
- package/dist/commands/setup/live-queries/templates/liveQueriesListener.ts.template +137 -0
- package/dist/commands/setup/live-queries/templates/migration.sql.template +98 -0
- package/dist/commands/setup.js +2 -1
- package/package.json +11 -11
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { recordTelemetryAttributes } from "@cedarjs/cli-helpers";
|
|
2
|
+
const command = "live-queries";
|
|
3
|
+
const description = "Setup live query invalidation with Postgres notifications";
|
|
4
|
+
function builder(yargs) {
|
|
5
|
+
yargs.option("force", {
|
|
6
|
+
alias: "f",
|
|
7
|
+
default: false,
|
|
8
|
+
description: "Overwrite existing configuration",
|
|
9
|
+
type: "boolean"
|
|
10
|
+
}).option("verbose", {
|
|
11
|
+
alias: "v",
|
|
12
|
+
default: false,
|
|
13
|
+
description: "Print more logs",
|
|
14
|
+
type: "boolean"
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
async function handler(options) {
|
|
18
|
+
recordTelemetryAttributes({
|
|
19
|
+
command: "setup live-queries",
|
|
20
|
+
force: options.force,
|
|
21
|
+
verbose: options.verbose
|
|
22
|
+
});
|
|
23
|
+
const { handler: handler2 } = await import("./liveQueriesHandler.js");
|
|
24
|
+
return handler2(options);
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
builder,
|
|
28
|
+
command,
|
|
29
|
+
description,
|
|
30
|
+
handler
|
|
31
|
+
};
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Listr } from "listr2";
|
|
4
|
+
import { addApiPackages } from "@cedarjs/cli-helpers";
|
|
5
|
+
import { getMigrationsPath, getSchemaPath } from "@cedarjs/project-config";
|
|
6
|
+
import { errorTelemetry } from "@cedarjs/telemetry";
|
|
7
|
+
import c from "../../../lib/colors.js";
|
|
8
|
+
import { getPaths, transformTSToJS, writeFile } from "../../../lib/index.js";
|
|
9
|
+
import { isTypeScriptProject } from "../../../lib/project.js";
|
|
10
|
+
const getApiPackageJson = () => {
|
|
11
|
+
const apiPackageJsonPath = path.join(getPaths().api.base, "package.json");
|
|
12
|
+
return JSON.parse(fs.readFileSync(apiPackageJsonPath, "utf-8"));
|
|
13
|
+
};
|
|
14
|
+
const hasPackage = (packageJson, packageName) => {
|
|
15
|
+
return Boolean(
|
|
16
|
+
packageJson.dependencies?.[packageName] || packageJson.devDependencies?.[packageName]
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
const getPrismaProvider = async () => {
|
|
20
|
+
try {
|
|
21
|
+
const prismaConfigPath = getPaths().api.prismaConfig;
|
|
22
|
+
if (!prismaConfigPath) {
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
let schemaPath = await getSchemaPath(prismaConfigPath);
|
|
26
|
+
if (!schemaPath) {
|
|
27
|
+
return void 0;
|
|
28
|
+
}
|
|
29
|
+
let stat;
|
|
30
|
+
try {
|
|
31
|
+
stat = fs.statSync(schemaPath);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
stat = void 0;
|
|
34
|
+
}
|
|
35
|
+
if (stat && stat.isDirectory()) {
|
|
36
|
+
const candidate = path.join(schemaPath, "schema.prisma");
|
|
37
|
+
if (fs.existsSync(candidate)) {
|
|
38
|
+
schemaPath = candidate;
|
|
39
|
+
} else {
|
|
40
|
+
return void 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!fs.existsSync(schemaPath)) {
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
const content = fs.readFileSync(schemaPath, "utf-8");
|
|
47
|
+
const match = content.match(/^\s*provider\s*=\s*["']([^"']+)["']/im);
|
|
48
|
+
if (match && match[1]) {
|
|
49
|
+
return match[1].toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
return void 0;
|
|
52
|
+
} catch {
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const findExistingLiveQueryMigration = ({ migrationsDirectoryPath }) => {
|
|
57
|
+
if (!fs.existsSync(migrationsDirectoryPath)) {
|
|
58
|
+
return void 0;
|
|
59
|
+
}
|
|
60
|
+
const globPattern = path.join(migrationsDirectoryPath, "*", "migration.sql").replaceAll("\\", "/");
|
|
61
|
+
const migrationFilePaths = fs.globSync(globPattern);
|
|
62
|
+
return migrationFilePaths.find((migrationFilePath) => {
|
|
63
|
+
const content = fs.readFileSync(migrationFilePath, "utf-8");
|
|
64
|
+
return content.includes("cedar_notify_table_change");
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
const generateMigrationFolderName = () => {
|
|
68
|
+
const now = /* @__PURE__ */ new Date();
|
|
69
|
+
const year = String(now.getFullYear());
|
|
70
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
71
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
72
|
+
const hour = String(now.getHours()).padStart(2, "0");
|
|
73
|
+
const minute = String(now.getMinutes()).padStart(2, "0");
|
|
74
|
+
const second = String(now.getSeconds()).padStart(2, "0");
|
|
75
|
+
return `${year}${month}${day}${hour}${minute}${second}_live_queries_notifications`;
|
|
76
|
+
};
|
|
77
|
+
const addLiveQueryListenerToGraphqlHandler = ({ force }) => {
|
|
78
|
+
const graphqlHandlerPath = path.join(
|
|
79
|
+
getPaths().api.functions,
|
|
80
|
+
`graphql.${isTypeScriptProject() ? "ts" : "js"}`
|
|
81
|
+
);
|
|
82
|
+
if (!fs.existsSync(graphqlHandlerPath)) {
|
|
83
|
+
return {
|
|
84
|
+
skipped: true,
|
|
85
|
+
reason: "GraphQL handler not found"
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const contentLines = fs.readFileSync(graphqlHandlerPath, "utf-8").split("\n");
|
|
89
|
+
const importLineRegex = /^import {.*startLiveQueryListener.*} from ['"]src\/lib\/liveQueriesListener['"];?$/;
|
|
90
|
+
const multilineImportRegex = /^} from ['"]src\/lib\/liveQueriesListener['"];?$/;
|
|
91
|
+
const hasImport = contentLines.some((line) => {
|
|
92
|
+
return importLineRegex.test(line) || multilineImportRegex.test(line);
|
|
93
|
+
});
|
|
94
|
+
const hasStartCall = contentLines.some(
|
|
95
|
+
(line) => line.trim().startsWith("startLiveQueryListener(") || line.trim().startsWith("void startLiveQueryListener(")
|
|
96
|
+
);
|
|
97
|
+
if (hasImport && hasStartCall && !force) {
|
|
98
|
+
return {
|
|
99
|
+
skipped: true,
|
|
100
|
+
reason: "Listener is already wired into GraphQL handler"
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const handlerIndex = contentLines.findLastIndex(
|
|
104
|
+
(line) => line === "export const handler = createGraphQLHandler({"
|
|
105
|
+
);
|
|
106
|
+
if (handlerIndex === -1) {
|
|
107
|
+
return {
|
|
108
|
+
skipped: true,
|
|
109
|
+
reason: "Unexpected syntax. Handler not found"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const lastImportIndex = contentLines.slice(0, handlerIndex).findLastIndex((line) => line.startsWith("import "));
|
|
113
|
+
if (lastImportIndex === -1) {
|
|
114
|
+
return {
|
|
115
|
+
skipped: true,
|
|
116
|
+
reason: "Unexpected syntax. No imports found"
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (!hasImport) {
|
|
120
|
+
contentLines.splice(
|
|
121
|
+
lastImportIndex + 1,
|
|
122
|
+
0,
|
|
123
|
+
"import { startLiveQueryListener } from 'src/lib/liveQueriesListener'"
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const handlerIndexAfterImport = hasImport ? handlerIndex : handlerIndex + 1;
|
|
127
|
+
if (!hasStartCall) {
|
|
128
|
+
contentLines.splice(
|
|
129
|
+
handlerIndexAfterImport,
|
|
130
|
+
0,
|
|
131
|
+
"",
|
|
132
|
+
"void startLiveQueryListener()"
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
fs.writeFileSync(graphqlHandlerPath, contentLines.join("\n"));
|
|
136
|
+
return {
|
|
137
|
+
skipped: false
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
const handler = async ({ force }) => {
|
|
141
|
+
const projectIsTypescript = isTypeScriptProject();
|
|
142
|
+
const apiPackageJson = getApiPackageJson();
|
|
143
|
+
const migrationsPath = await getMigrationsPath(getPaths().api.prismaConfig);
|
|
144
|
+
const hasRealtimeDependency = hasPackage(apiPackageJson, "@cedarjs/realtime");
|
|
145
|
+
const hasPgDependency = hasPackage(apiPackageJson, "pg");
|
|
146
|
+
const ext = projectIsTypescript ? "ts" : "js";
|
|
147
|
+
const migrationTemplatePath = path.resolve(
|
|
148
|
+
import.meta.dirname,
|
|
149
|
+
"templates",
|
|
150
|
+
"migration.sql.template"
|
|
151
|
+
);
|
|
152
|
+
const listenerTemplatePath = path.resolve(
|
|
153
|
+
import.meta.dirname,
|
|
154
|
+
"templates",
|
|
155
|
+
"liveQueriesListener.ts.template"
|
|
156
|
+
);
|
|
157
|
+
const existingMigrationPath = findExistingLiveQueryMigration({
|
|
158
|
+
migrationsDirectoryPath: migrationsPath
|
|
159
|
+
});
|
|
160
|
+
const migrationDirPath = path.join(
|
|
161
|
+
migrationsPath,
|
|
162
|
+
generateMigrationFolderName()
|
|
163
|
+
);
|
|
164
|
+
const migrationPath = path.join(migrationDirPath, "migration.sql");
|
|
165
|
+
const listenerPath = path.join(
|
|
166
|
+
getPaths().api.lib,
|
|
167
|
+
`liveQueriesListener.${ext}`
|
|
168
|
+
);
|
|
169
|
+
const tasks = new Listr(
|
|
170
|
+
[
|
|
171
|
+
{
|
|
172
|
+
title: "Checking for @cedarjs/realtime in api workspace...",
|
|
173
|
+
task: () => {
|
|
174
|
+
if (!hasRealtimeDependency) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`@cedarjs/realtime is not installed in your api workspace. Please run ${c.highlight("yarn cedar setup realtime")} first.`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
title: "Checking that your database provider is PostgreSQL...",
|
|
183
|
+
task: async () => {
|
|
184
|
+
const prismaProvider = await getPrismaProvider();
|
|
185
|
+
const unsupportedProviders = /* @__PURE__ */ new Set([
|
|
186
|
+
"sqlite",
|
|
187
|
+
"mysql",
|
|
188
|
+
"mongodb",
|
|
189
|
+
"sqlserver",
|
|
190
|
+
"cockroachdb"
|
|
191
|
+
]);
|
|
192
|
+
if (prismaProvider && unsupportedProviders.has(prismaProvider)) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Only PostgreSQL is supported for now (found provider "${prismaProvider}").`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
...addApiPackages(["pg@^8.18.0"]),
|
|
201
|
+
title: "Adding pg dependency to your api side...",
|
|
202
|
+
skip: () => {
|
|
203
|
+
if (hasPgDependency) {
|
|
204
|
+
return "pg is already installed";
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
title: "Adding live query notification migration...",
|
|
210
|
+
task: () => {
|
|
211
|
+
const migrationTemplate = fs.readFileSync(
|
|
212
|
+
migrationTemplatePath,
|
|
213
|
+
"utf-8"
|
|
214
|
+
);
|
|
215
|
+
const targetPath = force && existingMigrationPath ? existingMigrationPath : migrationPath;
|
|
216
|
+
writeFile(targetPath, migrationTemplate, {
|
|
217
|
+
overwriteExisting: force
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
skip: () => {
|
|
221
|
+
if (existingMigrationPath && !force) {
|
|
222
|
+
const migrationPath2 = path.relative(
|
|
223
|
+
getPaths().base,
|
|
224
|
+
existingMigrationPath
|
|
225
|
+
);
|
|
226
|
+
return `Existing live query migration found: ${migrationPath2}`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
title: `Adding api/src/lib/liveQueriesListener.${ext}...`,
|
|
232
|
+
task: async () => {
|
|
233
|
+
const listenerTemplate = fs.readFileSync(
|
|
234
|
+
listenerTemplatePath,
|
|
235
|
+
"utf-8"
|
|
236
|
+
);
|
|
237
|
+
const listenerContent = projectIsTypescript ? listenerTemplate : await transformTSToJS(listenerPath, listenerTemplate);
|
|
238
|
+
writeFile(listenerPath, listenerContent, {
|
|
239
|
+
overwriteExisting: force
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
title: "Wiring listener startup into GraphQL handler...",
|
|
245
|
+
task: (_ctx, task) => {
|
|
246
|
+
const result = addLiveQueryListenerToGraphqlHandler({ force });
|
|
247
|
+
if (result.skipped) {
|
|
248
|
+
task.skip(result.reason);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
title: "One more thing...",
|
|
254
|
+
task: (_ctx, task) => {
|
|
255
|
+
task.title = `One more thing...
|
|
256
|
+
|
|
257
|
+
${c.success("\nLive query notifications configured!\n")}
|
|
258
|
+
|
|
259
|
+
Apply the migration to activate Postgres notifications:
|
|
260
|
+
${c.highlight("\n\xA0\xA0yarn cedar prisma migrate dev\n")}
|
|
261
|
+
|
|
262
|
+
Then run the API server and use @live queries with invalidation keys
|
|
263
|
+
based on your GraphQL types and fields.
|
|
264
|
+
`;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
],
|
|
268
|
+
{
|
|
269
|
+
rendererOptions: { collapseSubtasks: false }
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
try {
|
|
273
|
+
await tasks.run();
|
|
274
|
+
} catch (e) {
|
|
275
|
+
errorTelemetry(process.argv, e.message);
|
|
276
|
+
console.error(c.error(e.message));
|
|
277
|
+
process.exit(e?.exitCode || 1);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
export {
|
|
281
|
+
handler
|
|
282
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Client } from 'pg'
|
|
2
|
+
|
|
3
|
+
import { liveQueryStore } from '@cedarjs/realtime'
|
|
4
|
+
|
|
5
|
+
import { logger } from 'src/lib/logger'
|
|
6
|
+
|
|
7
|
+
const LIVE_QUERY_CHANNEL = 'table_change'
|
|
8
|
+
const RECONNECT_DELAY_MS = 5000
|
|
9
|
+
|
|
10
|
+
let client: Client | undefined
|
|
11
|
+
let reconnectTimeout: ReturnType<typeof setTimeout> | undefined
|
|
12
|
+
let started = false
|
|
13
|
+
let connectionGeneration = 0
|
|
14
|
+
|
|
15
|
+
interface GetKeysToInvalidateArgs {
|
|
16
|
+
table: string
|
|
17
|
+
recordId: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getKeysToInvalidate({ table, recordId }: GetKeysToInvalidateArgs) {
|
|
21
|
+
const keys = [
|
|
22
|
+
`Query.${table}`,
|
|
23
|
+
`Query.${table.toLocaleLowerCase()}`,
|
|
24
|
+
`${table}:${recordId}`
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
return keys
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function reconnect(generation: number) {
|
|
31
|
+
if (reconnectTimeout) {
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
reconnectTimeout = setTimeout(async () => {
|
|
36
|
+
reconnectTimeout = undefined
|
|
37
|
+
|
|
38
|
+
if (generation !== connectionGeneration) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await connect()
|
|
43
|
+
}, RECONNECT_DELAY_MS)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface NotificationPayload {
|
|
47
|
+
table: string
|
|
48
|
+
operation: string
|
|
49
|
+
recordId: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function onNotification(payload: string | undefined) {
|
|
53
|
+
if (!payload) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const parsed: NotificationPayload = JSON.parse(payload)
|
|
59
|
+
const keys = getKeysToInvalidate(parsed)
|
|
60
|
+
|
|
61
|
+
await liveQueryStore?.invalidate(keys)
|
|
62
|
+
|
|
63
|
+
logger.debug(
|
|
64
|
+
{ operation: parsed.operation, table: parsed.table, keys },
|
|
65
|
+
'Invalidated live query keys from Postgres notification'
|
|
66
|
+
)
|
|
67
|
+
} catch (error) {
|
|
68
|
+
logger.error(
|
|
69
|
+
{ error, payload },
|
|
70
|
+
'Failed to process Postgres notification payload'
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function connect() {
|
|
76
|
+
connectionGeneration = connectionGeneration + 1
|
|
77
|
+
const generation = connectionGeneration
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
if (reconnectTimeout) {
|
|
81
|
+
clearTimeout(reconnectTimeout)
|
|
82
|
+
reconnectTimeout = undefined
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (client) {
|
|
86
|
+
const previousClient = client
|
|
87
|
+
client = undefined
|
|
88
|
+
previousClient.removeAllListeners()
|
|
89
|
+
await previousClient.end().catch(() => {})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const nextClient = new Client({
|
|
93
|
+
connectionString: process.env.DATABASE_URL,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
nextClient.on('notification', async (msg) => {
|
|
97
|
+
await onNotification(msg.payload)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
nextClient.on('error', (error) => {
|
|
101
|
+
logger.error(
|
|
102
|
+
{ error },
|
|
103
|
+
'Postgres live query listener encountered an error'
|
|
104
|
+
)
|
|
105
|
+
reconnect(generation)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
nextClient.on('end', () => {
|
|
109
|
+
logger.warn('Postgres live query listener disconnected')
|
|
110
|
+
reconnect(generation)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
await nextClient.connect()
|
|
114
|
+
await nextClient.query(`LISTEN ${LIVE_QUERY_CHANNEL}`)
|
|
115
|
+
|
|
116
|
+
if (generation !== connectionGeneration) {
|
|
117
|
+
nextClient.removeAllListeners()
|
|
118
|
+
await nextClient.end().catch(() => {})
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
client = nextClient
|
|
123
|
+
logger.info('Postgres live query listener connected')
|
|
124
|
+
} catch (error) {
|
|
125
|
+
logger.error({ error }, 'Failed to connect Postgres live query listener')
|
|
126
|
+
reconnect(generation)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function startLiveQueryListener() {
|
|
131
|
+
if (started) {
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
started = true
|
|
136
|
+
await connect()
|
|
137
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION cedar_notify_table_change() RETURNS TRIGGER AS $$
|
|
2
|
+
DECLARE
|
|
3
|
+
record_id text;
|
|
4
|
+
BEGIN
|
|
5
|
+
IF (TG_OP = 'DELETE') THEN
|
|
6
|
+
record_id := OLD.id::text;
|
|
7
|
+
ELSE
|
|
8
|
+
record_id := NEW.id::text;
|
|
9
|
+
END IF;
|
|
10
|
+
|
|
11
|
+
PERFORM pg_notify(
|
|
12
|
+
'table_change',
|
|
13
|
+
json_build_object(
|
|
14
|
+
'table', TG_TABLE_NAME,
|
|
15
|
+
'operation', TG_OP,
|
|
16
|
+
'recordId', record_id
|
|
17
|
+
)::text
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
RETURN NULL;
|
|
21
|
+
END;
|
|
22
|
+
$$ LANGUAGE plpgsql;
|
|
23
|
+
|
|
24
|
+
CREATE OR REPLACE FUNCTION cedar_attach_notify_triggers() RETURNS void AS $$
|
|
25
|
+
DECLARE
|
|
26
|
+
table_record record;
|
|
27
|
+
BEGIN
|
|
28
|
+
FOR table_record IN
|
|
29
|
+
SELECT table_schema, table_name
|
|
30
|
+
FROM information_schema.tables
|
|
31
|
+
WHERE table_type = 'BASE TABLE'
|
|
32
|
+
AND table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
33
|
+
AND table_name <> '_prisma_migrations'
|
|
34
|
+
LOOP
|
|
35
|
+
EXECUTE format(
|
|
36
|
+
'DROP TRIGGER IF EXISTS cedar_notify_change_trigger ON %I.%I',
|
|
37
|
+
table_record.table_schema,
|
|
38
|
+
table_record.table_name
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
EXECUTE format(
|
|
42
|
+
'CREATE TRIGGER cedar_notify_change_trigger
|
|
43
|
+
AFTER INSERT OR UPDATE OR DELETE ON %I.%I
|
|
44
|
+
FOR EACH ROW EXECUTE FUNCTION cedar_notify_table_change()',
|
|
45
|
+
table_record.table_schema,
|
|
46
|
+
table_record.table_name
|
|
47
|
+
);
|
|
48
|
+
END LOOP;
|
|
49
|
+
END;
|
|
50
|
+
$$ LANGUAGE plpgsql;
|
|
51
|
+
|
|
52
|
+
SELECT cedar_attach_notify_triggers();
|
|
53
|
+
|
|
54
|
+
CREATE OR REPLACE FUNCTION cedar_event_on_table_create() RETURNS event_trigger AS $$
|
|
55
|
+
DECLARE
|
|
56
|
+
cmd record;
|
|
57
|
+
schema_name text;
|
|
58
|
+
table_name text;
|
|
59
|
+
BEGIN
|
|
60
|
+
FOR cmd IN SELECT * FROM pg_event_trigger_ddl_commands() LOOP
|
|
61
|
+
IF cmd.object_type = 'table' THEN
|
|
62
|
+
schema_name := cmd.schema_name;
|
|
63
|
+
table_name := regexp_replace(cmd.object_identity, '^.*\\.', '');
|
|
64
|
+
|
|
65
|
+
IF schema_name IS NULL THEN
|
|
66
|
+
CONTINUE;
|
|
67
|
+
END IF;
|
|
68
|
+
|
|
69
|
+
IF schema_name IN ('pg_catalog', 'information_schema') THEN
|
|
70
|
+
CONTINUE;
|
|
71
|
+
END IF;
|
|
72
|
+
|
|
73
|
+
IF table_name = '_prisma_migrations' THEN
|
|
74
|
+
CONTINUE;
|
|
75
|
+
END IF;
|
|
76
|
+
|
|
77
|
+
EXECUTE format(
|
|
78
|
+
'DROP TRIGGER IF EXISTS cedar_notify_change_trigger ON %I.%I',
|
|
79
|
+
schema_name,
|
|
80
|
+
table_name
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
EXECUTE format(
|
|
84
|
+
'CREATE TRIGGER cedar_notify_change_trigger
|
|
85
|
+
AFTER INSERT OR UPDATE OR DELETE ON %I.%I
|
|
86
|
+
FOR EACH ROW EXECUTE FUNCTION cedar_notify_table_change()',
|
|
87
|
+
schema_name,
|
|
88
|
+
table_name
|
|
89
|
+
);
|
|
90
|
+
END IF;
|
|
91
|
+
END LOOP;
|
|
92
|
+
END;
|
|
93
|
+
$$ LANGUAGE plpgsql;
|
|
94
|
+
|
|
95
|
+
CREATE EVENT TRIGGER cedar_on_create_table
|
|
96
|
+
ON ddl_command_end
|
|
97
|
+
WHEN TAG IN ('CREATE TABLE', 'CREATE TABLE AS')
|
|
98
|
+
EXECUTE FUNCTION cedar_event_on_table_create();
|
package/dist/commands/setup.js
CHANGED
|
@@ -8,6 +8,7 @@ import * as setupGenerator from "./setup/generator/generator.js";
|
|
|
8
8
|
import * as setupGraphql from "./setup/graphql/graphql.js";
|
|
9
9
|
import * as setupI18n from "./setup/i18n/i18n.js";
|
|
10
10
|
import * as setupJobs from "./setup/jobs/jobs.js";
|
|
11
|
+
import * as setupLiveQueries from "./setup/live-queries/liveQueries.js";
|
|
11
12
|
import * as setupMailer from "./setup/mailer/mailer.js";
|
|
12
13
|
import * as setupMiddleware from "./setup/middleware/middleware.js";
|
|
13
14
|
import * as setupMonitoring from "./setup/monitoring/monitoring.js";
|
|
@@ -20,7 +21,7 @@ import * as setupUploads from "./setup/uploads/uploads.js";
|
|
|
20
21
|
import * as setupVite from "./setup/vite/vite.js";
|
|
21
22
|
const command = "setup <command>";
|
|
22
23
|
const description = "Initialize project config and install packages";
|
|
23
|
-
const builder = (yargs) => yargs.command(setupAuth).command(setupCache).command(setupDeploy).command(setupDocker).command(setupGenerator).command(setupGraphql).command(setupI18n).command(setupJobs).command(setupMailer).command(setupMiddleware).command(setupMonitoring).command(setupPackage).command(setupRealtime).command(setupServerFile).command(setupTsconfig).command(setupUi).command(setupUploads).command(setupVite).demandCommand().middleware(detectRxVersion).epilogue(
|
|
24
|
+
const builder = (yargs) => yargs.command(setupAuth).command(setupCache).command(setupDeploy).command(setupDocker).command(setupGenerator).command(setupGraphql).command(setupI18n).command(setupJobs).command(setupLiveQueries).command(setupMailer).command(setupMiddleware).command(setupMonitoring).command(setupPackage).command(setupRealtime).command(setupServerFile).command(setupTsconfig).command(setupUi).command(setupUploads).command(setupVite).demandCommand().middleware(detectRxVersion).epilogue(
|
|
24
25
|
`Also see the ${terminalLink(
|
|
25
26
|
"CedarJS CLI Reference",
|
|
26
27
|
"https://cedarjs.com/docs/cli-commands#setup"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedarjs/cli",
|
|
3
|
-
"version": "3.0.0-canary.
|
|
3
|
+
"version": "3.0.0-canary.13430+f819694cc",
|
|
4
4
|
"description": "The CedarJS Command Line",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,15 +34,15 @@
|
|
|
34
34
|
"@babel/parser": "7.29.0",
|
|
35
35
|
"@babel/preset-typescript": "7.28.5",
|
|
36
36
|
"@babel/runtime-corejs3": "7.29.0",
|
|
37
|
-
"@cedarjs/api-server": "3.0.0-canary.
|
|
38
|
-
"@cedarjs/cli-helpers": "3.0.0-canary.
|
|
39
|
-
"@cedarjs/fastify-web": "3.0.0-canary.
|
|
40
|
-
"@cedarjs/internal": "3.0.0-canary.
|
|
41
|
-
"@cedarjs/prerender": "3.0.0-canary.
|
|
42
|
-
"@cedarjs/project-config": "3.0.0-canary.
|
|
43
|
-
"@cedarjs/structure": "3.0.0-canary.
|
|
44
|
-
"@cedarjs/telemetry": "3.0.0-canary.
|
|
45
|
-
"@cedarjs/web-server": "3.0.0-canary.
|
|
37
|
+
"@cedarjs/api-server": "3.0.0-canary.13430",
|
|
38
|
+
"@cedarjs/cli-helpers": "3.0.0-canary.13430",
|
|
39
|
+
"@cedarjs/fastify-web": "3.0.0-canary.13430",
|
|
40
|
+
"@cedarjs/internal": "3.0.0-canary.13430",
|
|
41
|
+
"@cedarjs/prerender": "3.0.0-canary.13430",
|
|
42
|
+
"@cedarjs/project-config": "3.0.0-canary.13430",
|
|
43
|
+
"@cedarjs/structure": "3.0.0-canary.13430",
|
|
44
|
+
"@cedarjs/telemetry": "3.0.0-canary.13430",
|
|
45
|
+
"@cedarjs/web-server": "3.0.0-canary.13430",
|
|
46
46
|
"@listr2/prompt-adapter-enquirer": "2.0.16",
|
|
47
47
|
"@opentelemetry/api": "1.9.0",
|
|
48
48
|
"@opentelemetry/core": "1.30.1",
|
|
@@ -106,5 +106,5 @@
|
|
|
106
106
|
"publishConfig": {
|
|
107
107
|
"access": "public"
|
|
108
108
|
},
|
|
109
|
-
"gitHead": "
|
|
109
|
+
"gitHead": "f819694cc2036c57e405c37e92ee1fc578521573"
|
|
110
110
|
}
|