@nakedev/go-scaffold 0.1.2 → 0.1.4

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 (131) hide show
  1. package/README.md +93 -14
  2. package/dist/commands/auth.js +129 -0
  3. package/dist/commands/create.js +3 -2
  4. package/dist/commands/generate.js +61 -2
  5. package/dist/commands/method.js +66 -15
  6. package/dist/commands/migration.js +34 -0
  7. package/dist/commands/rbac.js +103 -0
  8. package/dist/commands/remove.js +36 -20
  9. package/dist/commands/worker.js +75 -0
  10. package/dist/index.js +71 -7
  11. package/dist/prompts/create-wizard.js +6 -1
  12. package/dist/prompts/generate-wizard.js +8 -0
  13. package/dist/templates/auth-manifest.js +19 -0
  14. package/dist/templates/create-manifest.js +25 -0
  15. package/dist/templates/module-manifest.js +2 -0
  16. package/dist/templates/rbac-manifest.js +17 -0
  17. package/dist/templates/worker-manifest.js +12 -0
  18. package/dist/utils/auth-patcher.js +96 -0
  19. package/dist/utils/gocheck.js +65 -0
  20. package/dist/utils/main-patcher.js +8 -1
  21. package/dist/utils/method-patcher.js +80 -16
  22. package/dist/utils/migrations.js +30 -8
  23. package/dist/utils/module-location.js +22 -0
  24. package/dist/utils/naming.js +75 -12
  25. package/dist/utils/openapi-patcher.js +35 -1
  26. package/dist/utils/platform-patcher.js +59 -0
  27. package/dist/utils/rbac-patcher.js +277 -0
  28. package/dist/utils/smoke-run.js +31 -0
  29. package/dist/utils/version.js +24 -0
  30. package/package.json +15 -6
  31. package/scripts/smoke-test.mjs +2058 -0
  32. package/templates/add/auth/cmd/seed/main.go.hbs +76 -0
  33. package/templates/add/auth/docs/forgot-password.yaml.hbs +19 -0
  34. package/templates/add/auth/docs/google-callback.yaml.hbs +22 -0
  35. package/templates/add/auth/docs/google-login.yaml.hbs +7 -0
  36. package/templates/add/auth/docs/login.yaml.hbs +19 -0
  37. package/templates/add/auth/docs/logout.yaml.hbs +8 -0
  38. package/templates/add/auth/docs/refresh.yaml.hbs +15 -0
  39. package/templates/add/auth/docs/register.yaml.hbs +19 -0
  40. package/templates/add/auth/docs/reset-password.yaml.hbs +16 -0
  41. package/templates/add/auth/docs/schemas.yaml.hbs +58 -0
  42. package/templates/add/auth/docs/users-me-logout-all.yaml.hbs +9 -0
  43. package/templates/add/auth/docs/users-me-resend-verification.yaml.hbs +10 -0
  44. package/templates/add/auth/docs/users-me.yaml.hbs +12 -0
  45. package/templates/add/auth/docs/verify-email.yaml.hbs +16 -0
  46. package/templates/add/auth/internal/app/user/dto.go.hbs +77 -0
  47. package/templates/add/auth/internal/app/user/errors.go.hbs +36 -0
  48. package/templates/add/auth/internal/app/user/handler.go.hbs +235 -0
  49. package/templates/add/auth/internal/app/user/jwt.go.hbs +84 -0
  50. package/templates/add/auth/internal/app/user/model/identity.go.hbs +28 -0
  51. package/templates/add/auth/internal/app/user/model/user.go.hbs +22 -0
  52. package/templates/add/auth/internal/app/user/repository.go.hbs +84 -0
  53. package/templates/add/auth/internal/app/user/service.go.hbs +447 -0
  54. package/templates/add/auth/internal/app/user/service_test.go.hbs +237 -0
  55. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +159 -0
  56. package/templates/add/auth/internal/shared/middleware/auth.go.hbs +64 -0
  57. package/templates/add/auth/internal/shared/middleware/ratelimit.go.hbs +44 -0
  58. package/templates/add/auth/migrations/create_identities.down.sql.hbs +1 -0
  59. package/templates/add/auth/migrations/create_identities.up.sql.hbs +11 -0
  60. package/templates/add/auth/migrations/create_users.down.sql.hbs +1 -0
  61. package/templates/add/auth/migrations/create_users.up.sql.hbs +9 -0
  62. package/templates/add/rbac/docs/permissions.yaml.hbs +24 -0
  63. package/templates/add/rbac/docs/role-permissions.yaml.hbs +31 -0
  64. package/templates/add/rbac/docs/role.yaml.hbs +17 -0
  65. package/templates/add/rbac/docs/roles.yaml.hbs +43 -0
  66. package/templates/add/rbac/docs/schemas.yaml.hbs +37 -0
  67. package/templates/add/rbac/docs/user-set-role.yaml.hbs +23 -0
  68. package/templates/add/rbac/docs/user.yaml.hbs +16 -0
  69. package/templates/add/rbac/docs/users.yaml.hbs +23 -0
  70. package/templates/add/rbac/internal/app/role/dto.go.hbs +40 -0
  71. package/templates/add/rbac/internal/app/role/errors.go.hbs +39 -0
  72. package/templates/add/rbac/internal/app/role/handler.go.hbs +101 -0
  73. package/templates/add/rbac/internal/app/role/model/permission.go.hbs +9 -0
  74. package/templates/add/rbac/internal/app/role/model/role.go.hbs +18 -0
  75. package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +8 -0
  76. package/templates/add/rbac/internal/app/role/repository.go.hbs +96 -0
  77. package/templates/add/rbac/internal/app/role/service.go.hbs +210 -0
  78. package/templates/add/rbac/internal/app/role/service_test.go.hbs +119 -0
  79. package/templates/add/rbac/internal/shared/middleware/authz.go.hbs +88 -0
  80. package/templates/add/rbac/internal/shared/middleware/authz_test.go.hbs +88 -0
  81. package/templates/add/rbac/migrations/add_roles.down.sql.hbs +15 -0
  82. package/templates/add/rbac/migrations/add_roles.up.sql.hbs +35 -0
  83. package/templates/add/worker/cmd/worker/main.go.hbs +77 -0
  84. package/templates/add/worker/internal/platform/cache/redis.go.hbs +18 -0
  85. package/templates/add/worker/internal/platform/mail/mail.go.hbs +48 -0
  86. package/templates/add/worker/internal/platform/mail/task.go.hbs +52 -0
  87. package/templates/add/worker/internal/platform/queue/client.go.hbs +31 -0
  88. package/templates/add/worker/internal/platform/queue/server.go.hbs +68 -0
  89. package/templates/create/base/.env.example.hbs +20 -0
  90. package/templates/create/base/.github/workflows/ci.yml.hbs +19 -4
  91. package/templates/create/base/.gitignore.hbs +2 -0
  92. package/templates/create/base/AGENTS.md.hbs +10 -12
  93. package/templates/create/base/Makefile.hbs +39 -8
  94. package/templates/create/base/README.md.hbs +52 -12
  95. package/templates/create/base/cmd/api/main.go.hbs +26 -1
  96. package/templates/create/base/internal/platform/database/database.go.hbs +53 -0
  97. package/templates/create/base/internal/shared/config/config.go.hbs +43 -1
  98. package/templates/create/base/internal/shared/middleware/cors.go.hbs +29 -0
  99. package/templates/create/base/internal/shared/middleware/error.go.hbs +13 -1
  100. package/templates/create/base/migrations/embed.go.hbs +15 -0
  101. package/templates/create/features/docs/architecture.md.hbs +22 -0
  102. package/templates/create/features/docs/common/responses.yaml.hbs +15 -0
  103. package/templates/create/features/docs/observability/metrics.yaml.hbs +12 -0
  104. package/templates/create/features/docs/openapi.yaml.hbs +17 -5
  105. package/templates/create/features/docs/patterns.md.hbs +9 -6
  106. package/templates/create/features/docs/techstack.md.hbs +3 -0
  107. package/templates/create/features/observability/middleware/metrics.go.hbs +41 -0
  108. package/templates/create/features/observability/middleware/tracing.go.hbs +46 -0
  109. package/templates/create/features/observability/platform/telemetry/tracing.go.hbs +130 -0
  110. package/templates/generate/module/docs/item.yaml.hbs +3 -3
  111. package/templates/generate/module/handler.go.hbs +37 -6
  112. package/templates/generate/module/handler_test.go.hbs +113 -51
  113. package/templates/generate/module/migration.down.sql.hbs +1 -1
  114. package/templates/generate/module/migration.up.sql.hbs +1 -1
  115. package/templates/generate/module/minimal/handler.go.hbs +28 -4
  116. package/templates/generate/module/minimal/handler_test.go.hbs +5 -65
  117. package/templates/generate/module/minimal/service_test.go.hbs +39 -16
  118. package/templates/generate/module/model/model.go.hbs +7 -0
  119. package/templates/generate/module/permission.down.sql.hbs +5 -0
  120. package/templates/generate/module/permission.up.sql.hbs +4 -0
  121. package/templates/generate/module/repository_test.go.hbs +79 -0
  122. package/templates/generate/module/service.go.hbs +6 -4
  123. package/templates/generate/module/service_test.go.hbs +68 -17
  124. package/tests/integration/default-module.test.mjs +46 -0
  125. package/tests/integration/generator-naming.test.mjs +81 -0
  126. package/tests/integration/generator-unit-test-seams.test.mjs +91 -0
  127. package/tests/integration/legacy-method-compat.test.mjs +222 -0
  128. package/tests/integration/remove-module.test.mjs +58 -0
  129. package/tests/unit/naming.test.mjs +94 -0
  130. package/tests/unit/smoke-isolation.test.mjs +35 -0
  131. package/dist/utils/module-paths.js +0 -33
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.patchConfigForAuth = patchConfigForAuth;
7
+ exports.patchMainGoForAuth = patchMainGoForAuth;
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const marker_patch_1 = require("./marker-patch");
10
+ const IMPORT_MARKER = "// go-scaffold:imports";
11
+ const CONFIG_FIELDS_MARKER = "// go-scaffold:config-fields";
12
+ const CONFIG_LOAD_MARKER = "// go-scaffold:config-load";
13
+ const CONFIG_CHECKS_MARKER = "// go-scaffold:config-checks";
14
+ const PLATFORM_INIT_MARKER = "// go-scaffold:platform-init";
15
+ const MODEL_MARKER = "// go-scaffold:models";
16
+ const ROUTE_MARKER = "// go-scaffold:routes";
17
+ const SHUTDOWN_MARKER = "// go-scaffold:shutdown";
18
+ // patchConfigForAuth adds JWT/cookie/password-reset/Google OAuth fields to
19
+ // Config, the same marker-based text insertion patchConfigForWorker uses
20
+ // (config.go.hbs is only rendered once, at `create` — everything after that
21
+ // is a real file a human may have already edited).
22
+ function patchConfigForAuth(configGoPath) {
23
+ let content = fs_extra_1.default.readFileSync(configGoPath, "utf8");
24
+ const fieldsBlock = [
25
+ "JWTSecret string",
26
+ "JWTAccessTTL time.Duration",
27
+ "JWTRefreshTTL time.Duration",
28
+ "CookieSecure bool",
29
+ "",
30
+ "PasswordResetTTL time.Duration",
31
+ "PasswordResetURL string",
32
+ "",
33
+ "EmailVerifyTTL time.Duration",
34
+ "EmailVerifyURL string",
35
+ "",
36
+ "GoogleClientID string",
37
+ "GoogleClientSecret string",
38
+ "GoogleRedirectURL string",
39
+ ].join("\n");
40
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_FIELDS_MARKER, fieldsBlock, "JWTSecret string");
41
+ const loadBlock = [
42
+ 'JWTSecret: env("JWT_SECRET", "dev-secret-change-me"),',
43
+ 'JWTAccessTTL: time.Duration(envInt("JWT_ACCESS_TTL_MIN", 15)) * time.Minute,',
44
+ 'JWTRefreshTTL: time.Duration(envInt("JWT_REFRESH_TTL_MIN", 43200)) * time.Minute,',
45
+ 'CookieSecure: env("COOKIE_SECURE", "false") == "true",',
46
+ "",
47
+ 'PasswordResetTTL: time.Duration(envInt("PASSWORD_RESET_TTL_MIN", 30)) * time.Minute,',
48
+ 'PasswordResetURL: env("PASSWORD_RESET_URL", "http://localhost:3000/reset-password"),',
49
+ "",
50
+ 'EmailVerifyTTL: time.Duration(envInt("EMAIL_VERIFY_TTL_MIN", 1440)) * time.Minute,',
51
+ 'EmailVerifyURL: env("EMAIL_VERIFY_URL", "http://localhost:3000/verify-email"),',
52
+ "",
53
+ 'GoogleClientID: env("GOOGLE_CLIENT_ID", ""),',
54
+ 'GoogleClientSecret: env("GOOGLE_CLIENT_SECRET", ""),',
55
+ 'GoogleRedirectURL: env("GOOGLE_REDIRECT_URL", ""),',
56
+ ].join("\n");
57
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_LOAD_MARKER, loadBlock, 'JWTSecret: env("JWT_SECRET"');
58
+ fs_extra_1.default.writeFileSync(configGoPath, content);
59
+ }
60
+ // patchMainGoForAuth wires the user domain into cmd/api: its import, a
61
+ // queue.Client (needed for the forgot-password email — cmd/api itself never
62
+ // enqueued anything before this), its two models in the AutoMigrate call, a
63
+ // prod guard against the still-default JWT secret, and its route
64
+ // registration (the domain's own Handler.Register splits /auth public vs
65
+ // /users protected — main.go doesn't need to know that split, same
66
+ // convention as every other module).
67
+ function patchMainGoForAuth(mainGoPath, goModule) {
68
+ let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
69
+ const importLine = `"${goModule}/internal/app/user"`;
70
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, importLine, importLine);
71
+ const modelImportLine = `usermodel "${goModule}/internal/app/user/model"`;
72
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, modelImportLine, modelImportLine);
73
+ const queueImportLine = `"${goModule}/internal/platform/queue"`;
74
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, queueImportLine, queueImportLine);
75
+ const mailImportLine = `"${goModule}/internal/platform/mail"`;
76
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, mailImportLine, mailImportLine);
77
+ const checkBlock = [
78
+ 'if cfg.IsProd() && cfg.JWTSecret == "dev-secret-change-me" {',
79
+ '\tlogger.Error("JWT_SECRET is still the dev default — set a real secret before deploying with APP_ENV=production")',
80
+ "\tos.Exit(1)",
81
+ "}",
82
+ ].join("\n");
83
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_CHECKS_MARKER, checkBlock, "JWT_SECRET is still the dev default");
84
+ const queueInitBlock = ["q, err := queue.NewClient(cfg.RedisURL)", "if err != nil {", '\tlogger.Error("open queue", "error", err)', "\tos.Exit(1)", "}"].join("\n");
85
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, PLATFORM_INIT_MARKER, queueInitBlock, "q, err := queue.NewClient(cfg.RedisURL)");
86
+ const migrateLine1 = "&usermodel.User{},";
87
+ const migrateLine2 = "&usermodel.Identity{},";
88
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, MODEL_MARKER, migrateLine1, migrateLine1);
89
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, MODEL_MARKER, migrateLine2, migrateLine2);
90
+ const routeLine = "user.NewHandler(user.NewService(user.NewRepository(db), user.NewRedisTokenStore(rdb), mail.NewAsyncClient(q), cfg), cfg.JWTSecret, cfg.JWTRefreshTTL, cfg.CookieSecure, rdb).Register(api)";
91
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, ROUTE_MARKER, routeLine, routeLine);
92
+ content = content.replace(/\n\t_ = api \/\/ dropped once `generate module` registers the first route\n/, "\n");
93
+ const shutdownBlock = ["if err := q.Close(); err != nil {", '\tlogger.Error("close queue", "error", err)', "}"].join("\n");
94
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, SHUTDOWN_MARKER, shutdownBlock, "if err := q.Close()");
95
+ fs_extra_1.default.writeFileSync(mainGoPath, content);
96
+ }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.typeChecks = typeChecks;
7
+ exports.assertNoDrift = assertNoDrift;
8
+ const child_process_1 = require("child_process");
9
+ const picocolors_1 = __importDefault(require("picocolors"));
10
+ const version_1 = require("./version");
11
+ // typeChecks runs `go vet ./...` in projectRoot. Returns null when there's no
12
+ // Go toolchain on PATH — the caller can't conclude anything either way then,
13
+ // the same graceful skip as gofmtTree.
14
+ //
15
+ // `go vet` rather than `go build`: build skips _test.go files entirely, and the
16
+ // generated handler_test.go is exactly where a shared/ signature change lands
17
+ // first (it constructs the middleware chain by hand). vet type-checks tests
18
+ // too, so it sees what build would miss.
19
+ function typeChecks(projectRoot) {
20
+ try {
21
+ (0, child_process_1.execFileSync)("go", ["version"], { stdio: "ignore" });
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ try {
27
+ (0, child_process_1.execFileSync)("go", ["vet", "./..."], { cwd: projectRoot, stdio: ["ignore", "pipe", "pipe"] });
28
+ return { ok: true, output: "" };
29
+ }
30
+ catch (err) {
31
+ const e = err;
32
+ return { ok: false, output: `${e.stderr?.toString() ?? ""}${e.stdout?.toString() ?? ""}`.trim() };
33
+ }
34
+ }
35
+ // assertNoDrift is the second half of a before/after pair: given what
36
+ // typeChecks said *before* the files were written, it re-checks and fails
37
+ // loudly if generating is what broke the project.
38
+ //
39
+ // Why compare instead of just checking afterwards: a project can be mid-refactor
40
+ // (or simply not have run `go mod tidy` yet), and blaming the generator for a
41
+ // break it didn't cause is worse than staying quiet. Only the
42
+ // passed-before → broken-after transition is unambiguously ours.
43
+ //
44
+ // The failure this exists for: `generate` renders templates pinned to the
45
+ // shared/ layer that `create` emits, so once a project edits that layer — which
46
+ // is normal, expected work — the generated code stops compiling against it.
47
+ // Without this check that lands as a mystery build error some time later, in
48
+ // files the user never wrote.
49
+ function assertNoDrift(projectRoot, before, config) {
50
+ if (before === null || !before.ok)
51
+ return; // no Go here, or already broken — not ours to judge
52
+ const after = typeChecks(projectRoot);
53
+ if (after === null || after.ok)
54
+ return;
55
+ const scaffoldedWith = config.scaffoldVersion ?? "unknown (predates version stamping)";
56
+ throw new Error(`${picocolors_1.default.red("the generated code doesn't compile, but this project was fine a moment ago.")}\n\n` +
57
+ `The most likely cause is drift: this project's internal/shared layer has been edited\n` +
58
+ `since it was scaffolded, so the templates this CLI emits no longer match it.\n\n` +
59
+ ` scaffolded with: go-scaffold ${scaffoldedWith}\n` +
60
+ ` this CLI: go-scaffold ${(0, version_1.cliVersion)()}\n\n` +
61
+ `${picocolors_1.default.dim("go vet ./... says:")}\n${after.output}\n\n` +
62
+ `The generated files were left in place — reconcile them with your shared/ layer by\n` +
63
+ `hand, or undo (\`go-scaffold remove module <name>\` for a module) and generate again\n` +
64
+ `with a CLI version that matches this project.`);
65
+ }
@@ -17,6 +17,11 @@ const UNUSED_API_LINE = "_ = api // dropped once `generate module` registers the
17
17
  // unpatchMainGo removes precisely what patch added.
18
18
  function mainGoLines(patch) {
19
19
  const modelAlias = `${patch.pkg}model`; // every domain's model subpackage is named "model"
20
+ const handlerArgs = [`${patch.pkg}.NewService(${patch.pkg}.NewRepository(db))`];
21
+ if (patch.auth)
22
+ handlerArgs.push("cfg.JWTSecret");
23
+ if (patch.permission)
24
+ handlerArgs.push("authz");
20
25
  return {
21
26
  importLine: `"${patch.goModule}/internal/app/${patch.modulePath}"`,
22
27
  modelImportLine: `${modelAlias} "${patch.goModule}/internal/app/${patch.modulePath}/model"`,
@@ -24,7 +29,9 @@ function mainGoLines(patch) {
24
29
  // `api` is the one route group declared by main.go.hbs, prefixed with
25
30
  // whatever apiPrefix the project chose at create time (e.g. /v1, /api,
26
31
  // or none) — every module registers on it, there is no per-module choice.
27
- routeLine: `${patch.pkg}.NewHandler(${patch.pkg}.NewService(${patch.pkg}.NewRepository(db))).Register(api)`,
32
+ // `authz` only exists in main.go once `add rbac` has run — patch.permission
33
+ // is only ever set once that's already been verified by the caller.
34
+ routeLine: `${patch.pkg}.NewHandler(${handlerArgs.join(", ")}).Register(api)`,
28
35
  };
29
36
  }
30
37
  // patchMainGo wires a newly generated module into cmd/api/main.go: its
@@ -12,9 +12,12 @@ const DTO_MARKER = "// go-scaffold:dto";
12
12
  const REPO_INTERFACE_MARKER = "// go-scaffold:repository-interface";
13
13
  const REPO_IMPL_MARKER = "// go-scaffold:repository-methods";
14
14
  const SERVICE_MARKER = "// go-scaffold:service-methods";
15
+ const SERVICE_INTERFACE_MARKER = "// go-scaffold:service-interface";
15
16
  const HANDLER_ROUTES_MARKER = "// go-scaffold:handler-routes";
16
17
  const HANDLER_FUNCS_MARKER = "// go-scaffold:handler-funcs";
17
- const FAKE_REPO_MARKER = "// go-scaffold:fake-repo-methods";
18
+ const REPOSITORY_STUB_FIELDS_MARKER = "// go-scaffold:repository-stub-fields";
19
+ const REPOSITORY_STUB_METHODS_MARKER = "// go-scaffold:repository-stub-methods";
20
+ const LEGACY_FAKE_REPO_METHODS_MARKER = "// go-scaffold:fake-repo-methods";
18
21
  const UNUSED_G_LINE = "\t_ = g\n";
19
22
  // writeHandler ensures whatever packages the new handler code references are
20
23
  // imported (a minimal module starts with only "gin" imported) and drops the
@@ -62,6 +65,48 @@ function patchMethod(paths, naming, method, opts, goModule) {
62
65
  else {
63
66
  patchDelete(paths, method, goModule);
64
67
  }
68
+ patchHandlerServiceInterface(paths.handlerPath, naming, method, opts, goModule);
69
+ }
70
+ function patchHandlerServiceInterface(handlerPath, naming, method, opts, goModule) {
71
+ let signature;
72
+ let needsModel = true;
73
+ let needsUUID = false;
74
+ if (opts.type === "get" && opts.getMode === "all") {
75
+ signature = `${method.pascalName}(context.Context, int, int) ([]model.${naming.pascalName}, error)`;
76
+ }
77
+ else if (opts.type === "get") {
78
+ signature = `${method.pascalName}(context.Context, string) (*model.${naming.pascalName}, error)`;
79
+ }
80
+ else if (opts.type === "post") {
81
+ signature = `${method.pascalName}(context.Context, ${method.pascalName}Input) (*model.${naming.pascalName}, error)`;
82
+ }
83
+ else if (opts.type === "put" || opts.type === "patch") {
84
+ signature = `${method.pascalName}(context.Context, uuid.UUID) (*model.${naming.pascalName}, error)`;
85
+ needsUUID = true;
86
+ }
87
+ else {
88
+ signature = `${method.pascalName}(context.Context, uuid.UUID) error`;
89
+ needsModel = false;
90
+ needsUUID = true;
91
+ }
92
+ let handler = fs_extra_1.default.readFileSync(handlerPath, "utf8");
93
+ if (!(0, marker_patch_1.hasMarker)(handler, SERVICE_INTERFACE_MARKER)) {
94
+ // Projects scaffolded before the narrow service interface stored *Service
95
+ // directly. The concrete type already exposes generated methods, so there
96
+ // is no interface declaration to patch and no migration is required.
97
+ if (handler.includes("svc *Service"))
98
+ return;
99
+ throw new Error(`marker "${SERVICE_INTERFACE_MARKER}" not found and Handler does not use legacy *Service wiring`);
100
+ }
101
+ handler = (0, marker_patch_1.insertBeforeMarker)(handler, SERVICE_INTERFACE_MARKER, signature);
102
+ handler = (0, marker_patch_1.ensureImport)(handler, "context");
103
+ if (needsModel) {
104
+ handler = (0, marker_patch_1.ensureImport)(handler, `${goModule}/internal/app/${naming.pkg}/model`);
105
+ }
106
+ if (needsUUID) {
107
+ handler = (0, marker_patch_1.ensureImport)(handler, "github.com/google/uuid");
108
+ }
109
+ fs_extra_1.default.writeFileSync(handlerPath, handler);
65
110
  }
66
111
  function patchGetAll(paths, naming, method, goModule) {
67
112
  let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
@@ -117,22 +162,40 @@ function patchGetOne(paths, naming, method, rawField, goModule) {
117
162
  fs_extra_1.default.writeFileSync(paths.repositoryPath, repo);
118
163
  let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
119
164
  service = (0, marker_patch_1.insertBeforeMarker)(service, REPO_INTERFACE_MARKER, `FindBy${fieldPascal}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error)`);
120
- // the interface just grew, so the hand-written fakeRepo mock in
121
- // service_test.go needs a matching stub or the test file stops compiling
165
+ // The repository interface just grew, so the test double needs a matching
166
+ // method or focused service tests stop compiling. New projects use a
167
+ // function-backed stub; projects scaffolded before that refactor retain the
168
+ // old fakeRepo marker and error behavior.
122
169
  let serviceTest = fs_extra_1.default.readFileSync(paths.serviceTestPath, "utf8");
123
- serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, FAKE_REPO_MARKER, [
124
- // nolint: only matters for a minimal module (fakeRepo never instantiated
125
- // yet, so unused flags every one of its methods individually); harmless
126
- // no-op on a full module where fakeRepo is already in use.
127
- `//nolint:unused`,
128
- `func (f *fakeRepo) FindBy${fieldPascal}(context.Context, string) (*model.${naming.pascalName}, error) {`,
129
- `\tif f.err != nil {`,
130
- `\t\treturn nil, f.err`,
131
- `\t}`,
132
- `\treturn f.m, nil`,
133
- `}`,
134
- ``,
135
- ].join("\n"));
170
+ if ((0, marker_patch_1.hasMarker)(serviceTest, REPOSITORY_STUB_FIELDS_MARKER) &&
171
+ (0, marker_patch_1.hasMarker)(serviceTest, REPOSITORY_STUB_METHODS_MARKER)) {
172
+ serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, REPOSITORY_STUB_FIELDS_MARKER, `findBy${fieldPascal}Fn func(context.Context, string) (*model.${naming.pascalName}, error)`);
173
+ serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, REPOSITORY_STUB_METHODS_MARKER, [
174
+ `//nolint:unused`,
175
+ `func (s *repositoryStub) FindBy${fieldPascal}(ctx context.Context, value string) (*model.${naming.pascalName}, error) {`,
176
+ `\tif s.findBy${fieldPascal}Fn == nil {`,
177
+ `\t\tpanic("unexpected repository.FindBy${fieldPascal} call")`,
178
+ `\t}`,
179
+ `\treturn s.findBy${fieldPascal}Fn(ctx, value)`,
180
+ `}`,
181
+ ``,
182
+ ].join("\n"));
183
+ }
184
+ else if ((0, marker_patch_1.hasMarker)(serviceTest, LEGACY_FAKE_REPO_METHODS_MARKER)) {
185
+ serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, LEGACY_FAKE_REPO_METHODS_MARKER, [
186
+ `//nolint:unused`,
187
+ `func (f *fakeRepo) FindBy${fieldPascal}(context.Context, string) (*model.${naming.pascalName}, error) {`,
188
+ `\tif f.err != nil {`,
189
+ `\t\treturn nil, f.err`,
190
+ `\t}`,
191
+ `\treturn f.m, nil`,
192
+ `}`,
193
+ ``,
194
+ ].join("\n"));
195
+ }
196
+ else {
197
+ throw new Error(`neither current repository stub markers nor legacy "${LEGACY_FAKE_REPO_METHODS_MARKER}" found in ${paths.serviceTestPath}`);
198
+ }
136
199
  fs_extra_1.default.writeFileSync(paths.serviceTestPath, serviceTest);
137
200
  service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, [
138
201
  `func (s *Service) ${method.pascalName}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error) {`,
@@ -266,6 +329,7 @@ function markersPresent(handlerPath, servicePath) {
266
329
  const service = fs_extra_1.default.readFileSync(servicePath, "utf8");
267
330
  return ((0, marker_patch_1.hasMarker)(handler, HANDLER_ROUTES_MARKER) &&
268
331
  (0, marker_patch_1.hasMarker)(handler, HANDLER_FUNCS_MARKER) &&
332
+ ((0, marker_patch_1.hasMarker)(handler, SERVICE_INTERFACE_MARKER) || handler.includes("svc *Service")) &&
269
333
  (0, marker_patch_1.hasMarker)(service, SERVICE_MARKER) &&
270
334
  (0, marker_patch_1.hasMarker)(service, REPO_INTERFACE_MARKER));
271
335
  }
@@ -3,15 +3,37 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.nextMigrationSeq = nextMigrationSeq;
6
+ exports.newMigrationVersion = newMigrationVersion;
7
7
  const fs_extra_1 = __importDefault(require("fs-extra"));
8
- // golang-migrate sequential numbering: 000001, 000002, ...
9
- function nextMigrationSeq(migrationsDir) {
10
- const files = fs_extra_1.default.existsSync(migrationsDir) ? fs_extra_1.default.readdirSync(migrationsDir) : [];
11
- const nums = files
8
+ // golang-migrate timestamp numbering the same convention as the CLI's own
9
+ // `migrate create -ext sql -dir migrations -seq=false <name>`: a 14-digit
10
+ // UTC timestamp (YYYYMMDDHHMMSS). Two people branching from the same base and
11
+ // each adding a migration get different filenames instead of both claiming
12
+ // the next sequential number and colliding on merge. golang-migrate orders
13
+ // by the numeric prefix either way, and a 14-digit timestamp always sorts
14
+ // after any existing 6-digit sequential number, so a project with old-style
15
+ // numbers already in migrations/ is safe to keep generating into.
16
+ function newMigrationVersion(migrationsDir) {
17
+ const existing = new Set((fs_extra_1.default.existsSync(migrationsDir) ? fs_extra_1.default.readdirSync(migrationsDir) : [])
12
18
  .map((f) => f.match(/^(\d+)_/))
13
19
  .filter((m) => m !== null)
14
- .map((m) => parseInt(m[1], 10));
15
- const next = (nums.length ? Math.max(...nums) : 0) + 1;
16
- return String(next).padStart(6, "0");
20
+ .map((m) => m[1]));
21
+ let d = new Date();
22
+ let version = formatVersion(d);
23
+ // Vanishingly unlikely in normal (human-driven) use, but guard the
24
+ // same-second edge case rather than silently overwrite a sibling file.
25
+ while (existing.has(version)) {
26
+ d = new Date(d.getTime() + 1000);
27
+ version = formatVersion(d);
28
+ }
29
+ return version;
30
+ }
31
+ function formatVersion(d) {
32
+ const pad = (n) => String(n).padStart(2, "0");
33
+ return (String(d.getUTCFullYear()) +
34
+ pad(d.getUTCMonth() + 1) +
35
+ pad(d.getUTCDate()) +
36
+ pad(d.getUTCHours()) +
37
+ pad(d.getUTCMinutes()) +
38
+ pad(d.getUTCSeconds()));
17
39
  }
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.existingModulePackages = existingModulePackages;
7
+ exports.resolveProjectModuleNaming = resolveProjectModuleNaming;
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ const naming_1 = require("./naming");
11
+ function existingModulePackages(projectDir) {
12
+ const appDir = path_1.default.join(projectDir, "internal", "app");
13
+ if (!fs_extra_1.default.existsSync(appDir))
14
+ return [];
15
+ return fs_extra_1.default
16
+ .readdirSync(appDir, { withFileTypes: true })
17
+ .filter((entry) => entry.isDirectory())
18
+ .map((entry) => entry.name);
19
+ }
20
+ function resolveProjectModuleNaming(projectDir, rawName) {
21
+ return (0, naming_1.resolveExistingModuleNaming)(rawName, existingModulePackages(projectDir));
22
+ }
@@ -1,4 +1,7 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.pluralize = pluralize;
4
7
  exports.toPascalCase = toPascalCase;
@@ -13,18 +16,14 @@ exports.validateModuleName = validateModuleName;
13
16
  exports.normalizeApiPrefix = normalizeApiPrefix;
14
17
  exports.validateApiPrefix = validateApiPrefix;
15
18
  exports.resolveModuleNaming = resolveModuleNaming;
19
+ exports.resolveExistingModuleNaming = resolveExistingModuleNaming;
16
20
  exports.resolveMethodNaming = resolveMethodNaming;
17
- // ponytail: heuristic pluralizer, not a dependency — covers common English
18
- // nouns (order/user/category/address); irregular plurals still need a manual
19
- // rename in the generated file, add a dictionary if that becomes frequent.
21
+ const pluralize_1 = __importDefault(require("pluralize"));
22
+ // Pluralization must be idempotent because module names may come from the
23
+ // interactive prompt (singular) or a documented/scripted command (often
24
+ // plural). Keep this wrapper exported for callers that only need inflection.
20
25
  function pluralize(word) {
21
- if (/[sxz]$/.test(word) || /[^aeiou](ch|sh)$/.test(word))
22
- return word + "es";
23
- if (/[^aeiou]y$/.test(word))
24
- return word.slice(0, -1) + "ies";
25
- if (word.endsWith("s"))
26
- return word;
27
- return word + "s";
26
+ return pluralize_1.default.plural(word);
28
27
  }
29
28
  function toPascalCase(value) {
30
29
  return value
@@ -98,7 +97,7 @@ function assertNotGoKeyword(ident, role) {
98
97
  // shadowing the builtin in main.go). Returns true|message for inquirer, and
99
98
  // backs the assert in resolveModuleNaming — one source of truth for both.
100
99
  function validateModuleName(rawName) {
101
- const pkg = toPackageName(rawName);
100
+ const pkg = toPackageName(pluralize_1.default.singular(toKebabCase(rawName)));
102
101
  if (!pkg)
103
102
  return `invalid module name: "${rawName}" (must contain letters/numbers)`;
104
103
  if (/^[0-9]/.test(pkg)) {
@@ -134,15 +133,79 @@ function resolveModuleNaming(rawName) {
134
133
  const check = validateModuleName(rawName);
135
134
  if (check !== true)
136
135
  throw new Error(check);
136
+ const singular = pluralize_1.default.singular(toKebabCase(rawName));
137
+ const pkg = toPackageName(singular);
138
+ const plural = pluralize_1.default.plural(singular);
139
+ return {
140
+ name: singular,
141
+ pkg,
142
+ pascalName: toPascalCase(singular),
143
+ plural,
144
+ tableName: toDbName(plural),
145
+ errorPrefix: toDbName(singular).toUpperCase(),
146
+ };
147
+ }
148
+ function legacyPluralize(word) {
149
+ if (/[sxz]$/.test(word) || /[^aeiou](ch|sh)$/.test(word))
150
+ return word + "es";
151
+ if (/[^aeiou]y$/.test(word))
152
+ return word.slice(0, -1) + "ies";
153
+ if (word.endsWith("s"))
154
+ return word;
155
+ return word + "s";
156
+ }
157
+ function resolveLegacyModuleNaming(rawName) {
137
158
  const pkg = toPackageName(rawName);
159
+ const plural = legacyPluralize(pkg);
138
160
  return {
139
161
  name: pkg,
140
162
  pkg,
141
163
  pascalName: toPascalCase(pkg),
142
- plural: pluralize(pkg),
164
+ plural,
165
+ tableName: toDbName(plural),
143
166
  errorPrefix: pkg.toUpperCase(),
144
167
  };
145
168
  }
169
+ // Projects generated before canonical inflection used the raw input as the Go
170
+ // package name. Prefer the canonical package when present, but keep locating
171
+ // legacy plural packages so upgrade does not make method/remove commands lose
172
+ // sight of existing code. Two matches are unsafe: silently choosing one can
173
+ // patch or delete the wrong module.
174
+ function resolveExistingModuleNaming(rawName, existingPackages) {
175
+ const requestedLegacy = resolveLegacyModuleNaming(rawName);
176
+ const existing = new Set(existingPackages);
177
+ let canonical;
178
+ try {
179
+ canonical = resolveModuleNaming(rawName);
180
+ }
181
+ catch (error) {
182
+ if (existing.has(requestedLegacy.pkg))
183
+ return requestedLegacy;
184
+ throw error;
185
+ }
186
+ const aliases = existingPackages.filter((pkg) => {
187
+ if (pkg === canonical.pkg)
188
+ return false;
189
+ try {
190
+ return resolveModuleNaming(pkg).pkg === canonical.pkg;
191
+ }
192
+ catch {
193
+ return false;
194
+ }
195
+ });
196
+ const matches = [
197
+ ...(existing.has(canonical.pkg) ? [canonical.pkg] : []),
198
+ ...aliases,
199
+ ];
200
+ if (matches.length > 1) {
201
+ throw new Error(`ambiguous module "${rawName}": ${matches.map((pkg) => `internal/app/${pkg}`).join(" and ")} exist`);
202
+ }
203
+ if (matches[0] === canonical.pkg)
204
+ return canonical;
205
+ if (matches[0])
206
+ return resolveLegacyModuleNaming(matches[0]);
207
+ return canonical;
208
+ }
146
209
  function resolveMethodNaming(rawName) {
147
210
  const cleaned = rawName.trim();
148
211
  const pascalName = toPascalCase(cleaned);
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.patchOpenapiIndex = patchOpenapiIndex;
7
7
  exports.unpatchOpenapiIndex = unpatchOpenapiIndex;
8
+ exports.patchOpenapiIndexRaw = patchOpenapiIndexRaw;
8
9
  const fs_extra_1 = __importDefault(require("fs-extra"));
9
10
  const marker_patch_1 = require("./marker-patch");
10
11
  const PATHS_MARKER = "# go-scaffold:paths";
@@ -43,5 +44,38 @@ function patchOpenapiIndex(openapiPath, naming, apiPrefix) {
43
44
  function unpatchOpenapiIndex(openapiPath, naming, apiPrefix) {
44
45
  const content = fs_extra_1.default.readFileSync(openapiPath, "utf8");
45
46
  const { paths, schemas } = openapiLines(naming, apiPrefix);
46
- fs_extra_1.default.writeFileSync(openapiPath, (0, marker_patch_1.removeLines)(content, [...paths, ...schemas]));
47
+ const withoutKnownEntries = (0, marker_patch_1.removeLines)(content, [...paths, ...schemas]);
48
+ // `generate method` can add any number of module-specific path documents.
49
+ // Remove every two-line path/$ref block owned by this module, otherwise the
50
+ // index keeps dangling references after the docs folder is deleted.
51
+ const lines = withoutKnownEntries.split("\n");
52
+ const moduleRefPrefix = `./${naming.plural}/`;
53
+ const kept = [];
54
+ for (let i = 0; i < lines.length; i += 1) {
55
+ const current = lines[i];
56
+ const next = lines[i + 1];
57
+ const isPathKey = /^\s*\/[^:]+:\s*$/.test(current);
58
+ const refMatch = next?.match(/^\s*\$ref:\s*['"]([^'"]+)['"]\s*$/);
59
+ if (isPathKey && refMatch?.[1].startsWith(moduleRefPrefix)) {
60
+ i += 1;
61
+ continue;
62
+ }
63
+ kept.push(current);
64
+ }
65
+ fs_extra_1.default.writeFileSync(openapiPath, kept.join("\n"));
66
+ }
67
+ // patchOpenapiIndexRaw wires hand-written path docs into the index — used by
68
+ // `add auth`/`add rbac`, whose endpoints aren't a single CRUD resource so
69
+ // there's no ModuleNaming to derive lines from. Each entry is patched with
70
+ // its own sentinel (the path key) rather than one block for all of them, so
71
+ // re-running `add rbac` after a partial failure doesn't skip entries that
72
+ // never made it in.
73
+ function patchOpenapiIndexRaw(openapiPath, apiPrefix, entries) {
74
+ let content = fs_extra_1.default.readFileSync(openapiPath, "utf8");
75
+ for (const { urlPath, file } of entries) {
76
+ const key = `${apiPrefix ? `/${apiPrefix}` : ""}${urlPath}:`;
77
+ const block = `${key}\n $ref: '${file}'`;
78
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, PATHS_MARKER, block, key);
79
+ }
80
+ fs_extra_1.default.writeFileSync(openapiPath, content);
47
81
  }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.patchConfigForWorker = patchConfigForWorker;
7
+ exports.patchMainGoForWorker = patchMainGoForWorker;
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const marker_patch_1 = require("./marker-patch");
10
+ const IMPORT_MARKER = "// go-scaffold:imports";
11
+ const CONFIG_FIELDS_MARKER = "// go-scaffold:config-fields";
12
+ const CONFIG_LOAD_MARKER = "// go-scaffold:config-load";
13
+ const PLATFORM_INIT_MARKER = "// go-scaffold:platform-init";
14
+ const READYZ_MARKER = "// go-scaffold:readyz-checks";
15
+ const SHUTDOWN_MARKER = "// go-scaffold:shutdown";
16
+ // patchConfigForWorker adds RedisURL + SMTP_* fields to Config and their
17
+ // env() loads to Load() — via marker comments, the same text-insertion
18
+ // approach as patchMainGo, since config.go.hbs is only rendered once (at
19
+ // `create`) and everything after that is a real file a human may have
20
+ // already edited.
21
+ function patchConfigForWorker(configGoPath) {
22
+ let content = fs_extra_1.default.readFileSync(configGoPath, "utf8");
23
+ const fieldsBlock = ["RedisURL string", "", "SMTPHost string", "SMTPPort string", "SMTPUsername string", "SMTPPassword string", "SMTPFrom string"].join("\n");
24
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_FIELDS_MARKER, fieldsBlock, "RedisURL string");
25
+ const loadBlock = [
26
+ 'RedisURL: env("REDIS_URL", "redis://localhost:6379/0"),',
27
+ "",
28
+ 'SMTPHost: env("SMTP_HOST", ""),',
29
+ 'SMTPPort: env("SMTP_PORT", "587"),',
30
+ 'SMTPUsername: env("SMTP_USERNAME", ""),',
31
+ 'SMTPPassword: env("SMTP_PASSWORD", ""),',
32
+ 'SMTPFrom: env("SMTP_FROM", "no-reply@example.local"),',
33
+ ].join("\n");
34
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_LOAD_MARKER, loadBlock, 'RedisURL: env("REDIS_URL"');
35
+ fs_extra_1.default.writeFileSync(configGoPath, content);
36
+ }
37
+ // patchMainGoForWorker wires Redis into cmd/api: opened alongside the DB, and
38
+ // pinged as part of /readyz (so a Redis outage is caught the same way a DB
39
+ // outage already is). It does not create a queue.Client — nothing in cmd/api
40
+ // enqueues a task until some domain actually needs to (e.g. a future `add
41
+ // auth`'s forgot-password flow); an unused *queue.Client sitting in main()
42
+ // would just be dead weight until then.
43
+ function patchMainGoForWorker(mainGoPath, goModule) {
44
+ let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
45
+ const cacheImport = `"${goModule}/internal/platform/cache"`;
46
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, cacheImport, cacheImport);
47
+ const initBlock = ["rdb, err := cache.Open(cfg)", "if err != nil {", '\tlogger.Error("open redis", "error", err)', "\tos.Exit(1)", "}"].join("\n");
48
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, PLATFORM_INIT_MARKER, initBlock, "rdb, err := cache.Open(cfg)");
49
+ const readyzBlock = [
50
+ "if err := rdb.Ping(c.Request.Context()).Err(); err != nil {",
51
+ '\tc.JSON(http.StatusServiceUnavailable, gin.H{"status": "unavailable"})',
52
+ "\treturn",
53
+ "}",
54
+ ].join("\n");
55
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, READYZ_MARKER, readyzBlock, "if err := rdb.Ping(");
56
+ const shutdownBlock = ["if err := rdb.Close(); err != nil {", '\tlogger.Error("close redis", "error", err)', "}"].join("\n");
57
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, SHUTDOWN_MARKER, shutdownBlock, "if err := rdb.Close()");
58
+ fs_extra_1.default.writeFileSync(mainGoPath, content);
59
+ }