@rebasepro/server-postgres 0.12.1-canary.gf5f1d39 → 0.13.1-canary.g18cfeb7

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.
Files changed (112) hide show
  1. package/dist/PostgresBackendDriver.d.ts +49 -2
  2. package/dist/PostgresBootstrapper.d.ts +25 -1
  3. package/dist/auth/services.d.ts +21 -0
  4. package/dist/{src-BbFOPJ1S.js → auth-users-columns-Do9mw5Y5.js} +602 -200
  5. package/dist/auth-users-columns-Do9mw5Y5.js.map +1 -0
  6. package/dist/backup/backup-service.d.ts +10 -1
  7. package/dist/backup/pg-tools.d.ts +47 -0
  8. package/dist/backup-service-Bww-Lg0s.js +8999 -0
  9. package/dist/backup-service-Bww-Lg0s.js.map +1 -0
  10. package/dist/cli-helpers.d.ts +40 -1
  11. package/dist/connection-BuZ97wsr.js +250 -0
  12. package/dist/connection-BuZ97wsr.js.map +1 -0
  13. package/dist/connection.d.ts +42 -0
  14. package/dist/ensure-collection-policies-DMUdRQdM.js +57 -0
  15. package/dist/ensure-collection-policies-DMUdRQdM.js.map +1 -0
  16. package/dist/ensure-collection-tables-DSIxvLLD.js +694 -0
  17. package/dist/ensure-collection-tables-DSIxvLLD.js.map +1 -0
  18. package/dist/index.es.js +1701 -10167
  19. package/dist/index.es.js.map +1 -1
  20. package/dist/policy-CPkCqVTz.js +105 -0
  21. package/dist/policy-CPkCqVTz.js.map +1 -0
  22. package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
  23. package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
  24. package/dist/schema/auth-schema.d.ts +83 -144
  25. package/dist/schema/auth-users-columns.d.ts +97 -0
  26. package/dist/schema/ensure-collection-policies.d.ts +60 -0
  27. package/dist/schema/ensure-collection-tables.d.ts +45 -3
  28. package/dist/schema/generate-drizzle-schema-logic.d.ts +1 -1
  29. package/dist/schema/generate-postgres-ddl-logic.d.ts +137 -3
  30. package/dist/schema/generated-schema-staleness.d.ts +39 -0
  31. package/dist/schema/introspect-db-constraints.d.ts +57 -0
  32. package/dist/schema/introspect-db-logic.d.ts +94 -5
  33. package/dist/schema/introspect-db-queries.d.ts +119 -0
  34. package/dist/schema/introspect-db-structure.d.ts +263 -0
  35. package/dist/schema/introspect-db-types.d.ts +11 -0
  36. package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
  37. package/dist/security/rls-enforcement.d.ts +53 -2
  38. package/dist/services/FetchService.d.ts +14 -8
  39. package/dist/services/RelationService.d.ts +24 -1
  40. package/dist/services/channel-bus/index.d.ts +1 -7
  41. package/dist/services/collection-helpers.d.ts +36 -2
  42. package/dist/services/dataService.d.ts +5 -1
  43. package/dist/services/realtimeService.d.ts +25 -21
  44. package/dist/services/row-pipeline.d.ts +1 -1
  45. package/dist/{src-Zqwaw3P5.js → src-C_wvdMnl.js} +78 -145
  46. package/dist/src-C_wvdMnl.js.map +1 -0
  47. package/dist/utils/connection-string.d.ts +29 -0
  48. package/dist/utils/drizzle-conditions.d.ts +5 -4
  49. package/dist/utils/pg-error-utils.d.ts +35 -0
  50. package/dist/websocket-D0TBU3ia.js +530 -0
  51. package/dist/websocket-D0TBU3ia.js.map +1 -0
  52. package/package.json +17 -16
  53. package/src/PostgresAdapter.ts +21 -2
  54. package/src/PostgresBackendDriver.ts +169 -3
  55. package/src/PostgresBootstrapper.ts +233 -35
  56. package/src/auth/ensure-tables.ts +335 -81
  57. package/src/auth/services.ts +24 -2
  58. package/src/backup/backup-cli.ts +41 -2
  59. package/src/backup/backup-service.ts +38 -5
  60. package/src/backup/pg-tools.ts +96 -3
  61. package/src/cli-helpers.ts +92 -9
  62. package/src/cli.ts +219 -56
  63. package/src/collections/validate-relations.ts +136 -14
  64. package/src/connection.ts +73 -0
  65. package/src/data-transformer.ts +22 -6
  66. package/src/databasePoolManager.ts +5 -2
  67. package/src/history/ensure-history-table.ts +7 -0
  68. package/src/schema/auth-schema.ts +30 -19
  69. package/src/schema/auth-users-columns.ts +131 -0
  70. package/src/schema/doctor.ts +7 -5
  71. package/src/schema/ensure-collection-policies.ts +105 -0
  72. package/src/schema/ensure-collection-tables.test.ts +105 -9
  73. package/src/schema/ensure-collection-tables.ts +301 -44
  74. package/src/schema/generate-drizzle-schema-logic.ts +49 -7
  75. package/src/schema/generate-postgres-ddl-logic.ts +474 -27
  76. package/src/schema/generated-schema-staleness.ts +169 -0
  77. package/src/schema/introspect-db-constraints.ts +385 -0
  78. package/src/schema/introspect-db-inference.ts +18 -8
  79. package/src/schema/introspect-db-logic.ts +385 -71
  80. package/src/schema/introspect-db-queries.ts +326 -0
  81. package/src/schema/introspect-db-structure.ts +670 -0
  82. package/src/schema/introspect-db-types.ts +56 -0
  83. package/src/schema/introspect-db.ts +37 -80
  84. package/src/schema/introspect-runtime.test.ts +56 -8
  85. package/src/schema/introspect-runtime.ts +31 -9
  86. package/src/schema/non-sql-collections.test.ts +131 -0
  87. package/src/schema/rls-bootstrap-sql.ts +288 -0
  88. package/src/security/anonymous-grants.test.ts +4 -2
  89. package/src/security/policy-drift.test.ts +11 -3
  90. package/src/security/rls-enforcement.ts +141 -3
  91. package/src/services/BranchService.ts +71 -28
  92. package/src/services/FetchService.ts +100 -107
  93. package/src/services/PersistService.ts +34 -7
  94. package/src/services/RelationService.ts +249 -48
  95. package/src/services/channel-bus/index.ts +0 -9
  96. package/src/services/channel-history.ts +8 -0
  97. package/src/services/channel-presence.ts +6 -0
  98. package/src/services/collection-helpers.ts +69 -3
  99. package/src/services/dataService.ts +5 -1
  100. package/src/services/realtimeService.ts +38 -35
  101. package/src/services/row-pipeline.ts +1 -1
  102. package/src/utils/connection-string.ts +58 -0
  103. package/src/utils/drizzle-conditions.ts +31 -6
  104. package/src/utils/pg-error-utils.ts +65 -0
  105. package/src/websocket.ts +18 -9
  106. package/dist/chunk-DSJWtz9O.js +0 -40
  107. package/dist/ensure-collection-tables-CNTcZGvn.js +0 -304
  108. package/dist/ensure-collection-tables-CNTcZGvn.js.map +0 -1
  109. package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
  110. package/dist/src-BbFOPJ1S.js.map +0 -1
  111. package/dist/src-Zqwaw3P5.js.map +0 -1
  112. package/src/schema/auth-bootstrap-sql.ts +0 -47
@@ -18,6 +18,9 @@ import {
18
18
  buildBackupFilename,
19
19
  buildPgDumpArgs,
20
20
  buildPgDumpallGlobalsArgs,
21
+ buildRowSecurityPgOptions,
22
+ diagnoseRowSecurityDumpFailure,
23
+ type RowSecurityIdentity,
21
24
  buildPgRestoreArgs,
22
25
  buildPgRestoreListArgs,
23
26
  checkToolServerCompatibility,
@@ -140,6 +143,15 @@ export async function createDump(opts: {
140
143
  inheritStdio?: boolean;
141
144
  includeGlobals?: boolean;
142
145
  env?: Record<string, string | undefined>;
146
+ /**
147
+ * Dump with row security left on, reading as this identity.
148
+ *
149
+ * The escape hatch for a managed Postgres, where the dumping role owns
150
+ * nothing and has no `BYPASSRLS`. Off by default, and deliberately so: with
151
+ * row security on, `pg_dump` stops erroring on rows it cannot see and
152
+ * simply omits them. See {@link RowSecurityIdentity}.
153
+ */
154
+ rowSecurity?: RowSecurityIdentity;
143
155
  }): Promise<BackupResult> {
144
156
  const env = opts.env ?? process.env;
145
157
  const bin = resolvePgBinary("pg_dump", env);
@@ -159,13 +171,34 @@ export async function createDump(opts: {
159
171
  connectionString: opts.connectionString,
160
172
  outFile: localFile,
161
173
  excludeSchemas: opts.excludeSchemas,
162
- noOwner: opts.noOwner
174
+ noOwner: opts.noOwner,
175
+ rowSecurity: opts.rowSecurity
163
176
  });
164
177
 
165
- await execa(bin, args, {
166
- stdio: opts.inheritStdio ? "inherit" : "pipe",
167
- env: { ...(env as Record<string, string>) }
168
- });
178
+ // `PGOPTIONS` is the only way to set a GUC on a tool that takes no SQL.
179
+ // Built from the same object that added `--enable-row-security`, so the
180
+ // flag cannot travel without the identity that makes it safe.
181
+ const dumpEnv: Record<string, string> = { ...(env as Record<string, string>) };
182
+ if (opts.rowSecurity) {
183
+ dumpEnv.PGOPTIONS = [env.PGOPTIONS, buildRowSecurityPgOptions(opts.rowSecurity)]
184
+ .filter(Boolean).join(" ");
185
+ }
186
+
187
+ try {
188
+ await execa(bin, args, {
189
+ stdio: opts.inheritStdio ? "inherit" : "pipe",
190
+ env: dumpEnv
191
+ });
192
+ } catch (error) {
193
+ // The RLS failure names a table and no cause. Replace it with the
194
+ // cause and the two ways out; anything else is re-thrown untouched.
195
+ const diagnosis = diagnoseRowSecurityDumpFailure(error);
196
+ if (!diagnosis) throw error;
197
+ throw new BackupToolError(
198
+ diagnosis,
199
+ "Run `rebase db backup --help` for the flag, and read what it says about partial dumps."
200
+ );
201
+ }
169
202
 
170
203
  const sizeBytes = fs.existsSync(localFile) ? fs.statSync(localFile).size : 0;
171
204
 
@@ -6,6 +6,8 @@
6
6
  * not require a database.
7
7
  */
8
8
 
9
+ import { forLibpq } from "../utils/connection-string";
10
+
9
11
  /**
10
12
  * A parsed backup destination. `--out` (and the scheduled-backup config)
11
13
  * accepts either a local filesystem path or an object-storage URL.
@@ -171,6 +173,45 @@ export function joinStorageKey(prefix: string, fileName: string): string {
171
173
  return clean.length > 0 ? `${clean}/${fileName}` : fileName;
172
174
  }
173
175
 
176
+ /**
177
+ * The identity `pg_dump` reads rows as, when row security is left on.
178
+ *
179
+ * Not optional, and that is the whole design. `pg_dump --enable-row-security`
180
+ * on its own is the dangerous command in this file: it turns the "query would
181
+ * be affected by row-level security policy" *error* into a dump that exits 0
182
+ * and is silently missing every row the dumping role's policies exclude. A
183
+ * backup that looks fine and restores most of your data is worse than one that
184
+ * refused to run.
185
+ *
186
+ * So the flag is unreachable without a subject to evaluate the policies
187
+ * against. Rebase's generated policies read `app.uid` and `app.user_roles`;
188
+ * supplying an admin role satisfies the `admin_full_access` rule and the dump
189
+ * sees everything that rule sees.
190
+ */
191
+ export interface RowSecurityIdentity {
192
+ /** Written to `app.uid`. Any non-empty value — it is only an audit trail. */
193
+ uid: string;
194
+ /** Written to `app.user_roles`. Must include a role the policies admit. */
195
+ roles: string[];
196
+ }
197
+
198
+ /**
199
+ * `PGOPTIONS` carrying an identity, for a libpq tool that has no other way to
200
+ * set a GUC.
201
+ *
202
+ * A backslash escape rather than quoting, which is what libpq's `-c` parsing
203
+ * takes: a space inside a value ends the option otherwise, so a role list is
204
+ * comma-joined and never spaced.
205
+ */
206
+ export function buildRowSecurityPgOptions(identity: RowSecurityIdentity): string {
207
+ const escape = (value: string) => value.replace(/([\\ ])/g, "\\$1");
208
+ return [
209
+ `-c app.uid=${escape(identity.uid)}`,
210
+ `-c app.user_id=${escape(identity.uid)}`,
211
+ `-c app.user_roles=${escape(identity.roles.join(","))}`
212
+ ].join(" ");
213
+ }
214
+
174
215
  /**
175
216
  * Assemble the `pg_dump` argument vector. Uses the custom format (`-Fc`),
176
217
  * which is compressed and restorable selectively via `pg_restore`.
@@ -182,18 +223,70 @@ export function buildPgDumpArgs(opts: {
182
223
  excludeSchemas?: string[];
183
224
  /** Number of parallel jobs (directory format only; ignored for -Fc). */
184
225
  noOwner?: boolean;
226
+ /**
227
+ * Dump with row security on, as this identity. Omit — which is the default
228
+ * — and `pg_dump` errors rather than skipping rows it cannot see.
229
+ */
230
+ rowSecurity?: RowSecurityIdentity;
185
231
  }): string[] {
186
232
  const args = ["--format=custom", "--no-password", `--file=${opts.outFile}`];
187
233
  if (opts.noOwner) {
188
234
  args.push("--no-owner");
189
235
  }
236
+ if (opts.rowSecurity) {
237
+ args.push("--enable-row-security");
238
+ }
190
239
  for (const schema of opts.excludeSchemas ?? []) {
191
240
  args.push(`--exclude-schema=${schema}`);
192
241
  }
193
- args.push(opts.connectionString);
242
+ args.push(forLibpq(opts.connectionString));
194
243
  return args;
195
244
  }
196
245
 
246
+ /**
247
+ * Whether a `pg_dump` failure is the row-security one, and what to do about it.
248
+ *
249
+ * The error text names the table and nothing else, so the first read of it is
250
+ * "why would a backup be affected by RLS at all?" — the answer being that the
251
+ * dumping role is not the tables' owner and has no `BYPASSRLS`, which is the
252
+ * normal state of the `postgres` user on Cloud SQL, RDS and every other managed
253
+ * Postgres. Nothing about that is visible from the message.
254
+ *
255
+ * Returns `null` for any other failure, so the caller reports it unchanged.
256
+ */
257
+ export function diagnoseRowSecurityDumpFailure(error: unknown): string | null {
258
+ const text = [
259
+ (error as { stderr?: unknown })?.stderr,
260
+ (error as { message?: unknown })?.message
261
+ ].map(part => (typeof part === "string" ? part : "")).join("\n");
262
+
263
+ if (!/row-level security policy/i.test(text)) return null;
264
+
265
+ const table = text.match(/for table "([^"]+)"/)?.[1];
266
+
267
+ return [
268
+ `pg_dump cannot read ${table ? `"${table}"` : "one of the tables"} because row-level security applies to it.`,
269
+ "",
270
+ " The dumping role is neither the table's owner nor `BYPASSRLS`, which is the normal",
271
+ " state of the `postgres` user on Cloud SQL, RDS and other managed Postgres — there is",
272
+ " no superuser to hand out.",
273
+ "",
274
+ " Two ways out:",
275
+ "",
276
+ " • Grant the dumping role BYPASSRLS, or make it the owner, and run this again. The",
277
+ " dump then contains every row, which is what a backup should mean.",
278
+ "",
279
+ " • Re-run with --enable-row-security to dump as an admin subject instead. Rebase",
280
+ " sets `app.uid`/`app.user_roles` so the generated `admin_full_access` policy",
281
+ " admits the dump. Read the warning it prints: the result contains exactly the",
282
+ " rows those policies admit, and any table whose policies do not include an",
283
+ " admin rule comes out short — with no error.",
284
+ "",
285
+ " Do not reach for a bare `pg_dump --enable-row-security` by hand. Without the",
286
+ " settings above it succeeds and silently omits rows."
287
+ ].join("\n");
288
+ }
289
+
197
290
  /**
198
291
  * Assemble the `pg_restore` argument vector for a custom-format dump.
199
292
  */
@@ -211,7 +304,7 @@ export function buildPgRestoreArgs(opts: {
211
304
  exitOnError?: boolean;
212
305
  noOwner?: boolean;
213
306
  }): string[] {
214
- const args = ["--format=custom", "--no-password", `--dbname=${opts.connectionString}`];
307
+ const args = ["--format=custom", "--no-password", `--dbname=${forLibpq(opts.connectionString)}`];
215
308
  if (opts.clean) {
216
309
  args.push("--clean", "--if-exists");
217
310
  }
@@ -255,7 +348,7 @@ export function buildPgDumpallGlobalsArgs(opts: {
255
348
  "--no-role-passwords",
256
349
  "--no-password",
257
350
  `--file=${opts.outFile}`,
258
- `--dbname=${opts.connectionString}`
351
+ `--dbname=${forLibpq(opts.connectionString)}`
259
352
  ];
260
353
  }
261
354
 
@@ -2,6 +2,8 @@ import { isManyToMany } from "@rebasepro/types";
2
2
  import path from "path";
3
3
  import fs from "fs";
4
4
  import { execSync } from "child_process";
5
+ import { createRequire } from "module";
6
+ import readline from "readline";
5
7
  import { pathToFileURL } from "url";
6
8
  import chalk from "chalk";
7
9
  import { logger } from "@rebasepro/server";
@@ -10,6 +12,49 @@ import { moduleDir as __helpersDirname } from "./module-dir";
10
12
 
11
13
 
12
14
 
15
+ /**
16
+ * Why is a dependency's binary missing — never installed, or installed with its
17
+ * build script blocked?
18
+ *
19
+ * These need opposite advice, and getting it wrong is not a cosmetic miss. pnpm
20
+ * 10+ refuses to run a dependency's lifecycle scripts unless it is allowlisted
21
+ * (`pnpm.onlyBuiltDependencies`, or `allowBuilds` in `pnpm-workspace.yaml`).
22
+ * `@ariga/atlas` downloads its platform binary in `preinstall`, so a blocked
23
+ * script leaves a state that looks like a successful install: the package is on
24
+ * disk with its `install.js` and `package.json`, `node_modules/.bin` is empty,
25
+ * the install exits 0, and the only signal is `ERR_PNPM_IGNORED_BUILDS` several
26
+ * screens up.
27
+ *
28
+ * Telling somebody in that state to install the package again sends them round
29
+ * the same loop forever — the add succeeds, the script is blocked again,
30
+ * nothing changes. Verified by doing it: `pnpm add @ariga/atlas` into a bare
31
+ * project yields exactly this, three "Failed to create bin … ENOENT" warnings
32
+ * and no binary.
33
+ *
34
+ * Resolution is attempted from the user's project first and this package
35
+ * second, matching the order {@link resolveLocalBin} searches — the driver may
36
+ * be installed a level up from where the command runs.
37
+ */
38
+ export function diagnoseMissingBin(packageName: string): "not-installed" | "build-script-blocked" {
39
+ const bases = [
40
+ pathToFileURL(path.join(process.cwd(), "package.json")),
41
+ pathToFileURL(path.join(__helpersDirname, "package.json"))
42
+ ];
43
+
44
+ for (const base of bases) {
45
+ try {
46
+ // `package.json` rather than the package root: a package with an
47
+ // `exports` map that omits `.` is unresolvable by name even when it
48
+ // is perfectly installed, which would misreport it as absent.
49
+ createRequire(base).resolve(`${packageName}/package.json`);
50
+ return "build-script-blocked";
51
+ } catch {
52
+ // Try the next base.
53
+ }
54
+ }
55
+ return "not-installed";
56
+ }
57
+
13
58
  export function resolveLocalBin(binName: string): string | null {
14
59
  // Try to find node_modules/.bin upwards from __helpersDirname first (package-relative)
15
60
  let dir = __helpersDirname;
@@ -41,10 +86,16 @@ export function resolveLocalBin(binName: string): string | null {
41
86
  return null;
42
87
  }
43
88
 
44
- export async function getTableIncludesFromCollections(collections: CollectionConfig[]): Promise<string[]> {
45
- const { getTableName, resolveCollectionRelations } = await import("@rebasepro/common");
89
+ export async function getTableIncludesFromCollections(allCollections: CollectionConfig[]): Promise<string[]> {
90
+ const { getTableName, resolveCollectionRelations, relationalCollections } = await import("@rebasepro/common");
46
91
  const { isPostgresCollectionConfig } = await import("@rebasepro/types");
47
92
 
93
+ // The include list is the inverse of {@link getTableExcludes}: a name on it
94
+ // is a name Atlas is allowed to drop. A Firestore collection called
95
+ // `exercises` must not put `public.exercises` here — a real, unrelated table
96
+ // of that name would lose its protection and be dropped by the next
97
+ // auto-approved `db push`.
98
+ const collections = relationalCollections(allCollections);
48
99
  const includes: string[] = [];
49
100
  for (const col of collections) {
50
101
  const tableName = getTableName(col);
@@ -204,13 +255,20 @@ export async function getTableExcludes(
204
255
  const queryTables = deps.queryExistingTables ?? queryExistingTables;
205
256
 
206
257
  const includes = await getIncludes(collectionsPath);
207
- // Framework-owned schemas Rebase manages itself — the declarative apply
208
- // must never touch them. `auth` holds the helper functions; `rebase` holds
209
- // the auth tables (users, refresh_tokens, …) and Atlas's own revision
210
- // table. Excluding both the schema object AND its contents keeps Atlas
211
- // from planning a `DROP SCHEMA … CASCADE` — which on a live database would
212
- // take the user/auth tables with it. `atlas_schema_revisions.*` is kept
213
- // for backwards-compatibility with any external revision schema.
258
+ // Schemas the declarative apply must never touch. `rebase` holds the auth
259
+ // tables (users, refresh_tokens, …), the RLS helper functions and Atlas's
260
+ // own revision table. Excluding both the schema object AND its contents
261
+ // keeps Atlas from planning a `DROP SCHEMA … CASCADE` — which on a live
262
+ // database would take the user/auth tables with it.
263
+ // `atlas_schema_revisions.*` is kept for backwards-compatibility with any
264
+ // external revision schema.
265
+ //
266
+ // `auth` stays on this list even though Rebase no longer creates it, and
267
+ // that is the point: on a database still holding the pre-1.0 schema it must
268
+ // survive until the guarded cleanup retires it, and on a Supabase database
269
+ // it is somebody else's and must never be in scope at all. Removing it here
270
+ // would let a `db push` plan `DROP SCHEMA auth CASCADE` against a live
271
+ // Supabase installation.
214
272
  const excludes: string[] = ["atlas_schema_revisions.*", "auth", "auth.*", "rebase", "rebase.*"];
215
273
 
216
274
  let existingTables: string[];
@@ -232,3 +290,28 @@ export async function getTableExcludes(
232
290
  return excludes;
233
291
  }
234
292
 
293
+
294
+ /**
295
+ * Ask a yes/no question on an interactive terminal.
296
+ *
297
+ * The `isTTY` guard is the contract, not an optimisation: non-interactive
298
+ * shells (CI, pipes, agents) can never answer, and `readline` on a
299
+ * non-TTY stdin resolves with whatever the pipe happens to contain — or never
300
+ * resolves at all. Returning false there is what makes an unattended
301
+ * `db push` abort instead of auto-confirming a destructive change. Callers
302
+ * should already have gone through {@link decidePushSafety}, which decides
303
+ * whether interactive confirmation is even possible; this is the backstop.
304
+ *
305
+ * Lives here rather than in cli.ts so it is reachable from a test — cli.ts uses
306
+ * `import.meta` and cannot be imported by the jest runner.
307
+ */
308
+ export async function promptConfirm(question: string): Promise<boolean> {
309
+ if (!process.stdin.isTTY) return false;
310
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
311
+ try {
312
+ const answer: string = await new Promise((resolve) => rl.question(question, resolve));
313
+ return /^y(es)?$/i.test(answer.trim());
314
+ } finally {
315
+ rl.close();
316
+ }
317
+ }