gemstack-cli 0.2.5 → 0.3.0

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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -25
  3. data/README.md +6 -17
  4. data/exe/gemstack +8 -1
  5. metadata +6 -159
  6. data/lib/gemstack/cli/add_generator.rb +0 -239
  7. data/lib/gemstack/cli/app_generator.rb +0 -142
  8. data/lib/gemstack/cli/commands/db.rb +0 -135
  9. data/lib/gemstack/cli/commands/jobs.rb +0 -96
  10. data/lib/gemstack/cli/controller_generator.rb +0 -73
  11. data/lib/gemstack/cli/deploy_generator.rb +0 -102
  12. data/lib/gemstack/cli/doctor.rb +0 -308
  13. data/lib/gemstack/cli/generator.rb +0 -144
  14. data/lib/gemstack/cli/job_generator.rb +0 -48
  15. data/lib/gemstack/cli/migration_generator.rb +0 -52
  16. data/lib/gemstack/cli/policy_generator.rb +0 -35
  17. data/lib/gemstack/cli/project.rb +0 -53
  18. data/lib/gemstack/cli/resource_generator.rb +0 -170
  19. data/lib/gemstack/cli/resource_spec.rb +0 -95
  20. data/lib/gemstack/cli.rb +0 -304
  21. data/templates/app/Gemfile.tt +0 -44
  22. data/templates/app/README.md.tt +0 -29
  23. data/templates/app/app/controllers/application_controller.rb +0 -6
  24. data/templates/app/app/jobs/application_job.rb +0 -9
  25. data/templates/app/app/mailers/application_mailer.rb +0 -8
  26. data/templates/app/app/mailers/templates/dot_keep +0 -0
  27. data/templates/app/app/models/application_model.rb +0 -19
  28. data/templates/app/app/serializers/application_serializer.rb +0 -6
  29. data/templates/app/bin/gemstack +0 -7
  30. data/templates/app/config/app.rb.tt +0 -54
  31. data/templates/app/config/database.yml.tt +0 -61
  32. data/templates/app/config/environments/development.rb.tt +0 -35
  33. data/templates/app/config/environments/production.rb.tt +0 -40
  34. data/templates/app/config/environments/test.rb.tt +0 -21
  35. data/templates/app/config/puma.rb +0 -21
  36. data/templates/app/config/routes.rb +0 -10
  37. data/templates/app/config.ru +0 -6
  38. data/templates/app/db/migrations/dot_keep +0 -0
  39. data/templates/app/db/seeds.rb +0 -5
  40. data/templates/app/dot_env.example.tt +0 -19
  41. data/templates/app/dot_gitignore +0 -16
  42. data/templates/app/dot_node-version.tt +0 -1
  43. data/templates/app/dot_nvmrc.tt +0 -1
  44. data/templates/app/dot_ruby-version.tt +0 -1
  45. data/templates/app/dot_tool-versions.tt +0 -4
  46. data/templates/app/test/health_test.rb +0 -18
  47. data/templates/app/test/test_helper.rb.tt +0 -25
  48. data/templates/auth/app/controllers/api_tokens_controller.rb +0 -35
  49. data/templates/auth/app/controllers/email_verifications_controller.rb +0 -31
  50. data/templates/auth/app/controllers/password_resets_controller.rb +0 -40
  51. data/templates/auth/app/controllers/registrations_controller.rb +0 -19
  52. data/templates/auth/app/controllers/sessions_controller.rb +0 -29
  53. data/templates/auth/app/mailers/auth_mailer.rb +0 -20
  54. data/templates/auth/app/mailers/templates/auth_mailer/email_verification.html.erb +0 -2
  55. data/templates/auth/app/mailers/templates/auth_mailer/email_verification.text.erb +0 -2
  56. data/templates/auth/app/mailers/templates/auth_mailer/password_reset.html.erb +0 -3
  57. data/templates/auth/app/mailers/templates/auth_mailer/password_reset.text.erb +0 -6
  58. data/templates/auth/app/models/auth_token.rb +0 -15
  59. data/templates/auth/app/models/user.rb +0 -12
  60. data/templates/auth/app/policies/application_policy.rb +0 -6
  61. data/templates/auth/app/serializers/api_token_serializer.rb +0 -6
  62. data/templates/auth/app/serializers/new_api_token_serializer.rb +0 -9
  63. data/templates/auth/app/serializers/user_serializer.rb +0 -6
  64. data/templates/auth/db/migrations/%timestamp%_create_auth_tables.rb +0 -41
  65. data/templates/auth/frontend/app/account/page.tsx +0 -116
  66. data/templates/auth/frontend/app/forgot-password/page.tsx +0 -48
  67. data/templates/auth/frontend/app/login/page.tsx +0 -26
  68. data/templates/auth/frontend/app/reset-password/page.tsx +0 -11
  69. data/templates/auth/frontend/app/signup/page.tsx +0 -27
  70. data/templates/auth/frontend/app/verify-email/page.tsx +0 -11
  71. data/templates/auth/frontend/components/auth/CredentialsForm.tsx +0 -58
  72. data/templates/auth/frontend/components/auth/ResetPasswordForm.tsx +0 -43
  73. data/templates/auth/frontend/components/auth/VerifyEmail.tsx +0 -28
  74. data/templates/auth/frontend/lib/auth.ts +0 -113
  75. data/templates/auth/test/controllers/auth_test.rb +0 -108
  76. data/templates/controller/app/controllers/%file_name%_controller.rb.tt +0 -10
  77. data/templates/controller/test/controllers/%file_name%_controller_test.rb.tt +0 -14
  78. data/templates/deploy/Caddyfile.tt +0 -19
  79. data/templates/deploy/Dockerfile.tt +0 -63
  80. data/templates/deploy/Procfile.tt +0 -6
  81. data/templates/deploy/compose.yaml.tt +0 -129
  82. data/templates/deploy/dot_dockerignore +0 -14
  83. data/templates/frontend/app/globals.css +0 -171
  84. data/templates/frontend/app/layout.tsx.tt +0 -19
  85. data/templates/frontend/app/page.module.css +0 -315
  86. data/templates/frontend/app/page.tsx.tt +0 -183
  87. data/templates/frontend/app/providers.tsx +0 -24
  88. data/templates/frontend/lib/gemstack/client.ts +0 -125
  89. data/templates/frontend/next-env.d.ts +0 -5
  90. data/templates/frontend/next.config.ts +0 -17
  91. data/templates/frontend/package.json.tt +0 -23
  92. data/templates/frontend/tsconfig.json +0 -21
  93. data/templates/job/app/jobs/%file_name%.rb.tt +0 -15
  94. data/templates/job/test/jobs/%file_name%_test.rb.tt +0 -15
  95. data/templates/migration/db/migrations/%timestamp%_%file_name%.rb.tt +0 -17
  96. data/templates/policy/app/policies/%file_name%_policy.rb.tt +0 -21
  97. data/templates/policy/test/policies/%file_name%_policy_test.rb.tt +0 -9
  98. data/templates/realtime/config/channels.rb +0 -14
  99. data/templates/realtime/frontend/lib/gemstack/realtime.ts +0 -120
  100. data/templates/resource/controller/app/controllers/%plural%_controller.rb.tt +0 -51
  101. data/templates/resource/controller/test/controllers/%plural%_controller_test.rb.tt +0 -74
  102. data/templates/resource/frontend/frontend/app/%url_segment%/[id]/edit/page.tsx.tt +0 -45
  103. data/templates/resource/frontend/frontend/app/%url_segment%/[id]/page.tsx.tt +0 -49
  104. data/templates/resource/frontend/frontend/app/%url_segment%/new/page.tsx.tt +0 -27
  105. data/templates/resource/frontend/frontend/app/%url_segment%/page.tsx.tt +0 -51
  106. data/templates/resource/frontend/frontend/components/%url_segment%/%class_name%Card.tsx.tt +0 -15
  107. data/templates/resource/frontend/frontend/components/%url_segment%/%class_name%Form.tsx.tt +0 -98
  108. data/templates/resource/frontend/frontend/components/%url_segment%/%class_name%Table.tsx.tt +0 -36
  109. data/templates/resource/frontend/frontend/lib/format.ts +0 -12
  110. data/templates/resource/frontend/frontend/lib/queries/%url_segment%.ts.tt +0 -64
  111. data/templates/resource/migration/db/migrations/%timestamp%_create_%table%.rb.tt +0 -18
  112. data/templates/resource/model/app/models/%file_name%.rb.tt +0 -15
  113. data/templates/resource/model/test/models/%file_name%_test.rb.tt +0 -29
  114. data/templates/resource/serializer/app/serializers/%file_name%_serializer.rb.tt +0 -7
  115. data/templates/storage/app/controllers/uploads_controller.rb.tt +0 -33
  116. data/templates/storage/app/serializers/upload_serializer.rb +0 -11
  117. data/templates/storage/frontend/lib/upload.ts +0 -52
  118. data/templates/storage/test/controllers/uploads_test.rb.tt +0 -30
@@ -1,36 +0,0 @@
1
- import Link from "next/link";
2
- import { display } from "@/lib/format";
3
- import type { <%= class_name %> } from "@/lib/api/generated";
4
-
5
- export function <%= class_name %>Table({ <%= spec.plural %> }: { <%= spec.plural %>: <%= class_name %>[] }) {
6
- if (<%= spec.plural %>.length === 0) return <p className="muted">No <%= spec.human_plural.downcase %> yet.</p>;
7
-
8
- return (
9
- <table className="table">
10
- <thead>
11
- <tr>
12
- <% table_fields.each do |field| -%>
13
- <th><%= field.label %></th>
14
- <% end -%>
15
- <% if spec.action?(:show) -%>
16
- <th aria-label="Actions" />
17
- <% end -%>
18
- </tr>
19
- </thead>
20
- <tbody>
21
- {<%= spec.plural %>.map((<%= spec.variable %>) => (
22
- <tr key={<%= spec.variable %>.id}>
23
- <% table_fields.each do |field| -%>
24
- <td>{display(<%= spec.variable %>.<%= field.column %>)}</td>
25
- <% end -%>
26
- <% if spec.action?(:show) -%>
27
- <td>
28
- <Link href={`/<%= spec.url_segment %>/${<%= spec.variable %>.id}`}>View</Link>
29
- </td>
30
- <% end -%>
31
- </tr>
32
- ))}
33
- </tbody>
34
- </table>
35
- );
36
- }
@@ -1,12 +0,0 @@
1
- // Display helpers shared by generated components.
2
-
3
- /** Human-readable text for any API value. */
4
- export function display(value: unknown): string {
5
- if (value === null || value === undefined || value === "") return "—";
6
- if (typeof value === "boolean") return value ? "Yes" : "No";
7
- if (typeof value === "object") return JSON.stringify(value);
8
- return String(value);
9
- }
10
-
11
- /** Field errors from an ApiError, keyed by attribute name. */
12
- export type FieldErrors = Record<string, string[]>;
@@ -1,64 +0,0 @@
1
- "use client";
2
-
3
- // TanStack Query hooks for <%= spec.human_plural.downcase %>, built on the generated API client.
4
- import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5
- import { <%= spec.client_name %> } from "@/lib/api/generated";
6
- import type { <%= [class_name, (spec.action?(:create) ? "#{class_name}Input" : nil), (spec.action?(:update) ? "#{class_name}UpdateInput" : nil)].compact.join(", ") %> } from "@/lib/api/generated";
7
-
8
- export const <%= spec.variable %>Keys = {
9
- all: ["<%= spec.plural %>"] as const,
10
- detail: (id: string | number) => ["<%= spec.plural %>", String(id)] as const,
11
- };
12
- <% if spec.action?(:index) -%>
13
-
14
- // perPage: records per page; omit it to use the API's default (config.http.pagination.per_page).
15
- export function use<%= spec.plural_class %>(page = 1, perPage?: number) {
16
- return useQuery({
17
- queryKey: [...<%= spec.variable %>Keys.all, "page", page, perPage],
18
- queryFn: () => <%= spec.client_name %>.list({ page, per_page: perPage }),
19
- placeholderData: keepPreviousData, // keep the current page visible while the next one loads
20
- });
21
- }
22
- <% end -%>
23
- <% if spec.action?(:show) -%>
24
-
25
- export function use<%= class_name %>(id: string | number) {
26
- return useQuery({ queryKey: <%= spec.variable %>Keys.detail(id), queryFn: () => <%= spec.client_name %>.get(id) });
27
- }
28
- <% end -%>
29
- <% if spec.action?(:create) -%>
30
-
31
- export function useCreate<%= class_name %>() {
32
- const queryClient = useQueryClient();
33
- return useMutation({
34
- mutationFn: (data: <%= class_name %>Input) => <%= spec.client_name %>.create(data),
35
- onSuccess: (<%= spec.variable %>: <%= class_name %>) => {
36
- queryClient.setQueryData(<%= spec.variable %>Keys.detail(<%= spec.variable %>.id), <%= spec.variable %>);
37
- return queryClient.invalidateQueries({ queryKey: <%= spec.variable %>Keys.all });
38
- },
39
- });
40
- }
41
- <% end -%>
42
- <% if spec.action?(:update) -%>
43
-
44
- export function useUpdate<%= class_name %>(id: string | number) {
45
- const queryClient = useQueryClient();
46
- return useMutation({
47
- mutationFn: (data: <%= class_name %>UpdateInput) => <%= spec.client_name %>.update(id, data),
48
- onSuccess: (<%= spec.variable %>: <%= class_name %>) => {
49
- queryClient.setQueryData(<%= spec.variable %>Keys.detail(id), <%= spec.variable %>);
50
- return queryClient.invalidateQueries({ queryKey: <%= spec.variable %>Keys.all });
51
- },
52
- });
53
- }
54
- <% end -%>
55
- <% if spec.action?(:destroy) -%>
56
-
57
- export function useDelete<%= class_name %>() {
58
- const queryClient = useQueryClient();
59
- return useMutation({
60
- mutationFn: (id: string | number) => <%= spec.client_name %>.delete(id),
61
- onSuccess: () => queryClient.invalidateQueries({ queryKey: <%= spec.variable %>Keys.all }),
62
- });
63
- }
64
- <% end -%>
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- Sequel.migration do
4
- change do
5
- create_table(:<%= spec.table %>) do
6
- primary_key :id, type: :Bignum
7
- <% fields.each do |field| -%>
8
- <%= migration_column(field) %>
9
- <% end -%>
10
- column :created_at, :timestamptz, null: false
11
- column :updated_at, :timestamptz, null: false
12
- <% fields.select { |f| f.index && !f.unique }.each do |field| -%>
13
-
14
- index :<%= field.column %>
15
- <% end -%>
16
- end
17
- end
18
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class <%= class_name %> < ApplicationModel
4
- <% fields.each do |field| -%>
5
- <%= model_field(field) %>
6
- <% end -%>
7
- <% fields.select(&:unique).each do |field| -%>
8
-
9
- validates :<%= field.column %>, uniqueness: true
10
- <% end -%>
11
- <% spec.references.each do |field| -%>
12
-
13
- belongs_to :<%= field.name %>
14
- <% end -%>
15
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../test_helper"
4
-
5
- class <%= class_name %>Test < GemStack::TestCase
6
- def sample = GemStack::DB::Testing.sample_attributes(<%= class_name %>)
7
-
8
- def test_valid_with_sample_attributes
9
- <%= spec.variable %> = <%= class_name %>.new(sample)
10
-
11
- assert_predicate <%= spec.variable %>, :valid?, <%= spec.variable %>.errors.to_h.inspect
12
- end
13
-
14
- def test_create
15
- <%= spec.variable %> = <%= class_name %>.create(sample)
16
-
17
- assert <%= spec.variable %>.id
18
- assert <%= spec.variable %>.created_at
19
- end
20
- <% fields.select(&:required?).each do |field| -%>
21
-
22
- def test_requires_<%= field.column %>
23
- <%= spec.variable %> = <%= class_name %>.new(sample.except(:<%= field.column %>))
24
-
25
- refute_predicate <%= spec.variable %>, :valid?
26
- assert_includes <%= spec.variable %>.errors[:<%= field.column %>], "is required"
27
- end
28
- <% end -%>
29
- end
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # What the API returns for a <%= class_name %>. Only listed attributes are exposed;
4
- # their types (from <%= class_name %>'s fields) drive the generated TypeScript types.
5
- class <%= class_name %>Serializer < ApplicationSerializer
6
- attributes :id, <%= fields.map { |f| ":#{f.column}" }.join(", ") %>, :created_at, :updated_at
7
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # POST /api/uploads — step 1 of a direct upload (frontend/lib/upload.ts):
4
- # validates the file's type and size and returns a short-lived URL the
5
- # browser PUTs the file to. Save the returned signed_id on your record:
6
- #
7
- # product.update(image_key: GemStack::Storage.key_for_signed_id(input[:image]))
8
- # GemStack::Storage.url(product.image_key) # signed download URL
9
- #
10
- # Allowed types and the size limit: config.storage (docs/storage.md).
11
- class UploadsController < ApplicationController
12
- <% if auth? -%>
13
- before :require_login
14
- <% else -%>
15
- # Anyone can upload until you add authentication (gemstack add auth), then:
16
- # before :require_login
17
- <% end -%>
18
- rate_limit to: 60, within: 3600, only: :create if respond_to?(:rate_limit)
19
-
20
- accepts :create do
21
- required :filename, :string, max_length: 255
22
- required :content_type, :string, max_length: 100
23
- required :byte_size, :integer, gt: 0
24
- end
25
- returns :create, UploadSerializer
26
-
27
- def create
28
- upload = GemStack::Storage.direct_upload(**input.slice(:filename, :content_type, :byte_size))
29
- result = UploadSerializer::Result.new(signed_id: upload[:signed_id], url: upload[:url],
30
- http_method: upload[:method], headers: upload[:headers])
31
- render result, status: :created, serializer: UploadSerializer
32
- end
33
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Where and how the browser sends the file (see UploadsController).
4
- class UploadSerializer < ApplicationSerializer
5
- Result = Data.define(:signed_id, :url, :http_method, :headers)
6
-
7
- attribute :signed_id, :string
8
- attribute :url, :string
9
- attribute(:method, :string) { |upload| upload.http_method }
10
- attribute :headers, :json
11
- end
@@ -1,52 +0,0 @@
1
- "use client";
2
-
3
- // Direct uploads (generated by `gemstack add storage`): the file goes from the
4
- // browser straight to storage (S3, or the Ruby disk endpoint in development).
5
- //
6
- // const { signedId } = await uploadFile(file, { onProgress: (p) => setProgress(p) });
7
- // await products.update(id, { image: signedId });
8
- import { useMutation } from "@tanstack/react-query";
9
- import { uploads } from "@/lib/api/generated";
10
-
11
- export interface UploadResult {
12
- /** Send this to the API to attach the file to a record. */
13
- signedId: string;
14
- }
15
-
16
- export interface UploadOptions {
17
- /** 0–1 while uploading. */
18
- onProgress?: (fraction: number) => void;
19
- signal?: AbortSignal;
20
- }
21
-
22
- export async function uploadFile(file: File, options: UploadOptions = {}): Promise<UploadResult> {
23
- const upload = await uploads.create({
24
- filename: file.name,
25
- content_type: file.type || "application/octet-stream",
26
- byte_size: file.size,
27
- });
28
- await send(upload.url, upload.method, upload.headers as Record<string, string>, file, options);
29
- return { signedId: upload.signed_id };
30
- }
31
-
32
- // XMLHttpRequest rather than fetch: it reports upload progress.
33
- function send(url: string, method: string, headers: Record<string, string>, file: File, options: UploadOptions) {
34
- return new Promise<void>((resolve, reject) => {
35
- const xhr = new XMLHttpRequest();
36
- xhr.open(method, url);
37
- for (const [name, value] of Object.entries(headers)) xhr.setRequestHeader(name, value);
38
- xhr.upload.onprogress = (event) => {
39
- if (event.lengthComputable) options.onProgress?.(event.loaded / event.total);
40
- };
41
- xhr.onload = () => (xhr.status < 300 ? resolve() : reject(new Error(`Upload failed (HTTP ${xhr.status})`)));
42
- xhr.onerror = () => reject(new Error("Upload failed: network error"));
43
- xhr.onabort = () => reject(new DOMException("Upload aborted", "AbortError"));
44
- options.signal?.addEventListener("abort", () => xhr.abort());
45
- xhr.send(file);
46
- });
47
- }
48
-
49
- /** uploadFile as a TanStack Query mutation: const upload = useUpload(); upload.mutateAsync(file). */
50
- export function useUpload(options: Omit<UploadOptions, "signal"> = {}) {
51
- return useMutation({ mutationFn: (file: File) => uploadFile(file, options) });
52
- }
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../test_helper"
4
-
5
- class UploadsTest < GemStack::TestCase
6
- <% if auth? -%>
7
- def setup
8
- sign_in_as(User.create(email: "uploader@example.com", password: "correct horse battery staple"))
9
- end
10
-
11
- <% end -%>
12
- def test_direct_upload
13
- post_json "/api/uploads", { filename: "cat.png", content_type: "image/png", byte_size: 4 }
14
-
15
- assert_status 201
16
- signed_id = json_body["signed_id"]
17
- put json_body["url"], "\x89PNG", "CONTENT_TYPE" => "image/png"
18
-
19
- assert_status 201
20
- key = GemStack::Storage.key_for_signed_id(signed_id)
21
- assert_equal "\x89PNG".b, GemStack::Storage.download(key).b
22
- end
23
-
24
- def test_refuses_other_types
25
- post_json "/api/uploads", { filename: "page.html", content_type: "text/html", byte_size: 10 }
26
-
27
- assert_error 422, "validation_failed"
28
- assert json_body["errors"]["content_type"]
29
- end
30
- end