s3arch 0.0.3 → 0.0.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd57db198f9598324792e16e5b84e9c22914ebfdfd4fddeb09fc99641458322c
4
- data.tar.gz: e107d35cd4b7084d3cf90e3d014bb4fe3c6089475b26419524d9132867565592
3
+ metadata.gz: 6e0ba51260081a3de075c6e14332909892693b64dce6eb1a3e52f7b1f145b537
4
+ data.tar.gz: 7553e90741ef8a1cc6c72b55bb9239c0e238da2a09916d34e8edf18780942a24
5
5
  SHA512:
6
- metadata.gz: 65aba03e3064bf597dc8bb4e53d7552c07f0b89ebb7ca24b9e6c74a7f5c22f9c95746cec237d1e100fce63cb8f285f782487300abd7f4d1b6e1c76553fdad5ff
7
- data.tar.gz: 28f5d435ce384c0c1c8e05d17d478ad9fdb9e0a949506350384886fdfcedbfaa2cbd9308dfd08d2faf91dfe4bf02ac07ac6d6749470143914a977f29ab0aef04
6
+ metadata.gz: 26e0cd3ff75ea9aff46dfd28a5978db557b26cef4780d966345b14fdb0b023bc9a6d22809c30c1422ac967aea3cbcc7bf18a75e94ea07d56af13d0f96d2d12e2
7
+ data.tar.gz: bf5c5b19de20e235f95427218b8beb0ee5ab03b4270bf31cbe38ca2f6f420c5665eb5b2a44e65461997a12412d4256099edaf3bc5be4ddb9f53f73105aaa3e79
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.5] - 2025-06-12
4
+
5
+ ### Added
6
+
7
+ - `S3arch::Dashboard` — mountable Rack application for viewing index stats (records, versions, owner counts)
8
+ - `S3arch::Dashboard::Application` — ERB-rendered HTML dashboard with mobile-friendly layout
9
+ - `S3arch::Holster` — Belt holster integration for convention-over-configuration mounting
10
+ - `S3arch::Routes` — Dispatcher mount DSL compatibility (`#routes` method)
11
+ - `S3arch::Web` — lightweight Rack entry point for Lambda integration
12
+ - `S3archController` — Belt controller for dashboard with Cognito auth gate
13
+ - `Configuration#dashboard_auth` — configurable authentication for dashboard access
14
+ - localStorage-based auth for CloudFront same-origin serving
15
+
16
+ ### Changed
17
+
18
+ - Dashboard renders HTML via ERB instead of JSON
19
+ - Scientific notation fix for large record/version numbers in dashboard
20
+
21
+ ## [0.0.4] - 2025-06-09
22
+
23
+ ### Fixed
24
+
25
+ - `Indexer#fetch_records` now uses `expression_attribute_names` for DynamoDB reserved words (e.g., `status`), fixing `ValidationException` on `rebuild()`
26
+
27
+ ### Added
28
+
29
+ - `Configuration#filter_fields` — declare which fields the `record_filter` proc needs projected from DynamoDB
30
+
31
+ ### Removed
32
+
33
+ - Hardcoded `filter_fields` method (`%w[status bin_id]`) — replaced by configurable `filter_fields`
34
+
3
35
  ## [0.0.2] - 2025-06-08
4
36
 
5
37
  ### Added
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ TerraDispatch.routes.draw do
4
+ namespace :s3arch, auth: :cognito, tables: [:search_indexes] do
5
+ get '/', action: 'index'
6
+ post '/rebuild', action: 'rebuild'
7
+ end
8
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ TerraDispatch.schema.define do
4
+ # S3arch does not define request/response models yet.
5
+ # The search_indexes table is provisioned by s3arch's own Terraform module.
6
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'aws-sdk-dynamodb'
5
+
6
+ class S3archController < BeltController::Base
7
+ VIEWS_PATH = File.expand_path('../../lib/s3arch/dashboard/views', __dir__)
8
+
9
+ def index
10
+ @owners = fetch_owners
11
+ html = render_erb('index')
12
+ html_response(html)
13
+ rescue StandardError => e
14
+ @error = e.message
15
+ @owners = []
16
+ html = render_erb('index')
17
+ html_response(html, 500)
18
+ end
19
+
20
+ def rebuild
21
+ owner_id = params['owner_id']&.strip
22
+ return error_response('owner_id is required', 400) if owner_id.nil? || owner_id.empty?
23
+
24
+ handler = S3arch.configuration.rebuild_handler
25
+ if handler
26
+ handler.call(owner_id)
27
+ else
28
+ S3arch::Indexer.new.rebuild(owner_id)
29
+ end
30
+ success_response(status: 'ok', owner_id: owner_id)
31
+ rescue StandardError => e
32
+ error_response("Failed to rebuild index: #{e.message}", 500)
33
+ end
34
+
35
+ private
36
+
37
+ def fetch_owners
38
+ dynamodb = Aws::DynamoDB::Client.new
39
+ config = S3arch.configuration
40
+ items = []
41
+ scan_params = { table_name: config.version_table }
42
+
43
+ loop do
44
+ result = dynamodb.scan(scan_params)
45
+ items.concat(result.items)
46
+ break unless result.last_evaluated_key
47
+
48
+ scan_params[:exclusive_start_key] = result.last_evaluated_key
49
+ end
50
+
51
+ owners = items.map do |item|
52
+ {
53
+ owner_id: item[config.owner_key] || item.values.first,
54
+ version: item['version'],
55
+ record_count: item['record_count'],
56
+ updated_at: item['updated_at']
57
+ }
58
+ end
59
+ owners.sort_by { |o| o[:updated_at].to_s }.reverse
60
+ end
61
+
62
+ def render_erb(template)
63
+ path = File.join(VIEWS_PATH, "#{template}.html.erb")
64
+ ERB.new(File.read(path), trim_mode: '-').result(binding)
65
+ end
66
+ end
@@ -30,6 +30,9 @@ module S3arch
30
30
  # Filter proc — receives a record hash, returns true to include in index
31
31
  attr_accessor :record_filter
32
32
 
33
+ # Fields needed by the record_filter for projection (e.g., %w[status])
34
+ attr_accessor :filter_fields
35
+
33
36
  # Owner extractor — proc that extracts owner_id from a DynamoDB stream record
34
37
  attr_accessor :owner_extractor
35
38
 
@@ -39,12 +42,18 @@ module S3arch
39
42
  # Searcher settings
40
43
  attr_accessor :version_ttl, :max_results, :max_cached_dbs, :ephemeral_storage_mb
41
44
 
45
+ # Custom rebuild handler — proc/lambda that receives owner_id.
46
+ # When set, the controller uses this instead of calling Indexer.new.rebuild directly.
47
+ # Useful for triggering rebuilds via Lambda invocation instead of in-process.
48
+ attr_accessor :rebuild_handler
49
+
42
50
  def initialize
43
51
  @owner_key = 'user_id'
44
52
  @searchable_fields = %w[name description]
45
53
  @token_field = 'searchTokens'
46
54
  @metadata_fields = %w[status created_at]
47
55
  @record_filter = ->(_record) { true }
56
+ @filter_fields = %w[status]
48
57
  @owner_extractor = lambda { |stream_record|
49
58
  image = stream_record.dig('dynamodb', 'NewImage') || stream_record.dig('dynamodb', 'OldImage') || {}
50
59
  image.dig(owner_key, 'S')
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'json'
5
+ require 'aws-sdk-dynamodb'
6
+
7
+ module S3arch
8
+ module Dashboard
9
+ class Application
10
+ VIEWS_PATH = File.expand_path('views', __dir__)
11
+
12
+ # Dispatcher mount DSL contract — keep in sync with #call dispatch below.
13
+ def routes
14
+ [
15
+ { method: :get, path: '/' },
16
+ { method: :post, path: '/rebuild' }
17
+ ]
18
+ end
19
+
20
+ def call(env)
21
+ req = Rack::Request.new(env)
22
+ path = req.path_info.sub(%r{^/}, '')
23
+
24
+ case [req.request_method, path]
25
+ when ['GET', ''], %w[GET index]
26
+ index(req)
27
+ when %w[POST rebuild]
28
+ rebuild(req)
29
+ else
30
+ [404, { 'content-type' => 'text/plain' }, ['Not Found']]
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def index(req)
37
+ @env = req.env
38
+ @owners = fetch_owners
39
+ html = render('index')
40
+ [200, { 'content-type' => 'text/html' }, [html]]
41
+ end
42
+
43
+ def rebuild(req)
44
+ owner_id = req.params['owner_id']&.strip
45
+ if owner_id && !owner_id.empty?
46
+ indexer = S3arch::Indexer.new
47
+ indexer.rebuild(owner_id)
48
+ end
49
+ [303, { 'location' => "#{req.script_name}/" }, []]
50
+ end
51
+
52
+ def fetch_owners
53
+ config = S3arch.configuration
54
+ dynamodb = Aws::DynamoDB::Client.new
55
+ items = []
56
+ params = { table_name: config.version_table }
57
+
58
+ loop do
59
+ result = dynamodb.scan(params)
60
+ items.concat(result.items)
61
+ break unless result.last_evaluated_key
62
+
63
+ params[:exclusive_start_key] = result.last_evaluated_key
64
+ end
65
+
66
+ owners = items.map do |item|
67
+ {
68
+ owner_id: item[config.owner_key] || item.values.first,
69
+ version: item['version'],
70
+ record_count: item['record_count'],
71
+ updated_at: item['updated_at']
72
+ }
73
+ end
74
+ owners.sort_by { |o| o[:updated_at].to_s }.reverse
75
+ rescue StandardError => e
76
+ @error = e.message
77
+ []
78
+ end
79
+
80
+ def render(template)
81
+ path = File.join(VIEWS_PATH, "#{template}.html.erb")
82
+ ERB.new(File.read(path), trim_mode: '-').result(binding)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,150 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>S3arch Dashboard</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <script>
7
+ (function() {
8
+ function getIdToken() {
9
+ try {
10
+ for (var i = 0; i < localStorage.length; i++) {
11
+ var key = localStorage.key(i);
12
+ if (key && key.indexOf('.idToken') !== -1) return localStorage.getItem(key);
13
+ }
14
+ } catch(e) {}
15
+ return null;
16
+ }
17
+ function isTokenValid(token) {
18
+ try {
19
+ var payload = JSON.parse(atob(token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/')));
20
+ return payload.exp && payload.exp > Date.now() / 1000;
21
+ } catch(e) { return false; }
22
+ }
23
+ var token = getIdToken();
24
+ if (!token || !isTokenValid(token)) {
25
+ window.location.replace('/login');
26
+ }
27
+ })();
28
+ </script>
29
+ <style>
30
+ * { box-sizing: border-box; margin: 0; padding: 0; }
31
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 1.5rem; }
32
+ h1 { margin-bottom: 1rem; font-size: 1.5rem; }
33
+ .error { background: #fee; border: 1px solid #fcc; padding: 0.75rem; border-radius: 4px; margin-bottom: 1rem; color: #c00; }
34
+ .success { background: #efe; border: 1px solid #cfc; padding: 0.75rem; border-radius: 4px; margin-bottom: 1rem; color: #060; display: none; }
35
+ .meta { font-size: 0.85rem; color: #666; margin-bottom: 1rem; }
36
+ .empty { padding: 2rem; text-align: center; color: #666; }
37
+
38
+ /* Desktop: table */
39
+ table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 6px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
40
+ th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid #eee; }
41
+ th { background: #fafafa; font-weight: 600; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.03em; color: #555; }
42
+ tr:last-child td { border-bottom: none; }
43
+ tr:hover td { background: #f9fafb; }
44
+ .btn { border: none; padding: 0.4rem 0.8rem; border-radius: 4px; cursor: pointer; font-size: 0.85rem; font-weight: 500; transition: background 0.15s; }
45
+ .btn-rebuild { background: #2563eb; color: #fff; }
46
+ .btn-rebuild:hover { background: #1d4ed8; }
47
+ .btn-rebuild:disabled { background: #93c5fd; cursor: not-allowed; }
48
+ .owner-id { font-family: "SF Mono", SFMono-Regular, Consolas, monospace; font-size: 0.8rem; }
49
+
50
+ /* Mobile: card layout */
51
+ @media (max-width: 640px) {
52
+ body { padding: 1rem; }
53
+ table, thead, tbody, th, tr, td { display: block; }
54
+ thead { display: none; }
55
+ tr { background: #fff; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 0.75rem; padding: 0.75rem; }
56
+ tr:hover td { background: transparent; }
57
+ td { padding: 0.25rem 0; border: none; display: flex; justify-content: space-between; align-items: center; }
58
+ td::before { content: attr(data-label); font-weight: 600; font-size: 0.75rem; text-transform: uppercase; color: #555; margin-right: 0.5rem; }
59
+ td:last-child { margin-top: 0.5rem; justify-content: flex-start; }
60
+ td:last-child::before { display: none; }
61
+ .btn { width: 100%; text-align: center; padding: 0.6rem; }
62
+ }
63
+ </style>
64
+ </head>
65
+ <body>
66
+ <h1>S3arch Dashboard</h1>
67
+ <p class="meta"><%= @owners.size %> indexed owner<%= @owners.size == 1 ? '' : 's' %></p>
68
+
69
+ <div id="flash" class="success"></div>
70
+
71
+ <%- if @error -%>
72
+ <div class="error"><strong>Error:</strong> <%= @error %></div>
73
+ <%- end -%>
74
+
75
+ <%- if @owners.any? -%>
76
+ <table>
77
+ <thead>
78
+ <tr>
79
+ <th>Owner</th>
80
+ <th>Version</th>
81
+ <th>Records</th>
82
+ <th>Last Updated</th>
83
+ <th>Actions</th>
84
+ </tr>
85
+ </thead>
86
+ <tbody>
87
+ <%- @owners.each do |owner| -%>
88
+ <tr>
89
+ <td class="owner-id" data-label="Owner"><%= owner[:owner_id] %></td>
90
+ <td data-label="Version"><%= owner[:version].to_i %></td>
91
+ <td data-label="Records"><%= owner[:record_count] ? owner[:record_count].to_i.to_s.gsub(/(\d)(?=(\d{3})+$)/, '\1,') : '—' %></td>
92
+ <td data-label="Updated"><%= owner[:updated_at] || '—' %></td>
93
+ <td>
94
+ <button class="btn btn-rebuild" onclick="rebuild('<%= owner[:owner_id] %>', this)">Recreate SQLite</button>
95
+ </td>
96
+ </tr>
97
+ <%- end -%>
98
+ </tbody>
99
+ </table>
100
+ <%- else -%>
101
+ <div class="empty">No indexed owners found.</div>
102
+ <%- end -%>
103
+
104
+ <script>
105
+ function getIdToken() {
106
+ try {
107
+ for (var i = 0; i < localStorage.length; i++) {
108
+ var key = localStorage.key(i);
109
+ if (key && key.indexOf('.idToken') !== -1) return localStorage.getItem(key);
110
+ }
111
+ } catch(e) {}
112
+ return null;
113
+ }
114
+
115
+ function rebuild(ownerId, btn) {
116
+ btn.disabled = true;
117
+ btn.textContent = 'Rebuilding\u2026';
118
+ var headers = { 'Content-Type': 'application/json' };
119
+ var token = getIdToken();
120
+ if (token) headers['Authorization'] = 'Bearer ' + token;
121
+
122
+ fetch(window.location.pathname.replace(/\/$/, '') + '/rebuild', {
123
+ method: 'POST',
124
+ headers: headers,
125
+ body: JSON.stringify({ owner_id: ownerId })
126
+ }).then(function(r) { return r.json(); }).then(function(data) {
127
+ btn.textContent = 'Recreate SQLite';
128
+ btn.disabled = false;
129
+ var flash = document.getElementById('flash');
130
+ if (data.status === 'ok') {
131
+ flash.className = 'success';
132
+ flash.textContent = 'Rebuild triggered for ' + ownerId;
133
+ flash.style.display = 'block';
134
+ } else {
135
+ flash.className = 'error';
136
+ flash.textContent = data.error || 'Rebuild failed';
137
+ flash.style.display = 'block';
138
+ }
139
+ }).catch(function(e) {
140
+ btn.textContent = 'Recreate SQLite';
141
+ btn.disabled = false;
142
+ var flash = document.getElementById('flash');
143
+ flash.className = 'error';
144
+ flash.textContent = 'Request failed: ' + e.message;
145
+ flash.style.display = 'block';
146
+ });
147
+ }
148
+ </script>
149
+ </body>
150
+ </html>
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'erb'
5
+ require_relative 'dashboard/application'
6
+
7
+ module S3arch
8
+ module Dashboard
9
+ def self.app
10
+ Application.new
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module S3arch
4
+ class Holster < Belt::Holster
5
+ self.gem_root = File.expand_path('../..', __dir__)
6
+ end
7
+ end
@@ -4,7 +4,6 @@ require 'aws-sdk-dynamodb'
4
4
  require 'aws-sdk-s3'
5
5
  require 'sqlite3'
6
6
  require 'json'
7
-
8
7
  module S3arch
9
8
  # Builds SQLite FTS5 databases per owner from pre-computed tokens stored in DynamoDB.
10
9
  # The indexer never sees raw content — only tokens. Supports incremental updates via
@@ -80,6 +79,8 @@ module S3arch
80
79
  { statusCode: 200, body: JSON.generate(rebuilt: grouped.size) }
81
80
  end
82
81
 
82
+ RESERVED_WORDS = Set.new(%w[status name comment count size type]).freeze
83
+
83
84
  private
84
85
 
85
86
  def group_changes(sqs_records)
@@ -244,11 +245,7 @@ module S3arch
244
245
  # Full rebuild: fetches token field from DynamoDB (never reads content)
245
246
  def fetch_records(owner_id)
246
247
  records = []
247
- fields_to_project = ['id', @config.token_field, @config.owner_key] + @config.metadata_fields + filter_fields
248
- params = { table_name: @config.source_table, index_name: @config.source_index,
249
- key_condition_expression: "#{@config.owner_key} = :owner",
250
- expression_attribute_values: { ':owner' => owner_id },
251
- projection_expression: fields_to_project.uniq.join(', ') }
248
+ params = build_query_params(owner_id)
252
249
 
253
250
  loop do
254
251
  result = @dynamodb.query(params)
@@ -268,17 +265,29 @@ module S3arch
268
265
  records
269
266
  end
270
267
 
271
- def extract_meta_from_item(item)
272
- @config.metadata_fields.to_h do |field|
273
- [field, item[field].to_s]
274
- end
268
+ def build_query_params(owner_id)
269
+ fields = (['id', @config.token_field, @config.owner_key] +
270
+ @config.metadata_fields + @config.filter_fields).uniq
271
+ expression_names = {}
272
+ projected = fields.map { |f| reserved_word?(f) ? "##{f}".tap { |p| expression_names[p] = f } : f }
273
+
274
+ owner_placeholder = reserved_word?(@config.owner_key) ? "##{@config.owner_key}" : @config.owner_key
275
+ expression_names["##{@config.owner_key}"] = @config.owner_key if reserved_word?(@config.owner_key)
276
+
277
+ params = { table_name: @config.source_table, index_name: @config.source_index,
278
+ key_condition_expression: "#{owner_placeholder} = :owner",
279
+ expression_attribute_values: { ':owner' => owner_id },
280
+ projection_expression: projected.join(', ') }
281
+ params[:expression_attribute_names] = expression_names if expression_names.any?
282
+ params
275
283
  end
276
284
 
277
- def filter_fields
278
- # Fields needed by the record_filter (best-effort — add status/bin_id for default filter)
279
- %w[status bin_id]
285
+ def extract_meta_from_item(item)
286
+ @config.metadata_fields.to_h { |field| [field, item[field].to_s] }
280
287
  end
281
288
 
289
+ def reserved_word?(field) = RESERVED_WORDS.include?(field.downcase)
290
+
282
291
  def build_database(db_path, records)
283
292
  FileUtils.rm_f(db_path)
284
293
  db = SQLite3::Database.new(db_path)
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module S3arch
4
+ # Lightweight route manifest for Dispatcher mount DSL.
5
+ # No heavy dependencies — safe to load in parse-only contexts.
6
+ module Routes
7
+ def self.routes
8
+ [
9
+ { method: :get, path: '/' },
10
+ { method: :post, path: '/rebuild' }
11
+ ]
12
+ end
13
+ end
14
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module S3arch
4
- VERSION = '0.0.3'
4
+ VERSION = '0.0.5'
5
5
  end
data/lib/s3arch/web.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Lightweight S3arch load for web/API contexts (no SQLite dependency).
4
+ # Provides configuration, holster registration, and routes only.
5
+ require_relative 'version'
6
+ require_relative 'configuration'
7
+ require_relative 'routes'
8
+ require_relative 'holster'
9
+
10
+ module S3arch
11
+ class Error < StandardError; end
12
+
13
+ class << self
14
+ def configuration
15
+ @configuration ||= Configuration.new
16
+ end
17
+
18
+ def configure
19
+ yield(configuration)
20
+ end
21
+
22
+ def reset_configuration!
23
+ @configuration = Configuration.new
24
+ end
25
+ end
26
+ end
data/lib/s3arch.rb CHANGED
@@ -6,6 +6,11 @@ require_relative 's3arch/tokenizer'
6
6
  require_relative 's3arch/indexer'
7
7
  require_relative 's3arch/searcher'
8
8
  require_relative 's3arch/handler'
9
+ require_relative 's3arch/routes'
10
+ require_relative 's3arch/dashboard'
11
+
12
+ # Register as a Belt holster when Belt is loaded
13
+ require_relative 's3arch/holster' if defined?(Belt::Holster)
9
14
 
10
15
  module S3arch
11
16
  class Error < StandardError; end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: s3arch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Dalton
@@ -64,6 +64,20 @@ dependencies:
64
64
  - - "~>"
65
65
  - !ruby/object:Gem::Version
66
66
  version: '1.0'
67
+ - !ruby/object:Gem::Dependency
68
+ name: rack
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '2.0'
74
+ type: :runtime
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '2.0'
67
81
  - !ruby/object:Gem::Dependency
68
82
  name: sqlite3
69
83
  requirement: !ruby/object:Gem::Requirement
@@ -78,8 +92,8 @@ dependencies:
78
92
  - - "~>"
79
93
  - !ruby/object:Gem::Version
80
94
  version: '2.0'
81
- description: Per-owner SQLite FTS5 indexes stored on S3, queried from Lambda /tmp.
82
- DynamoDB source records are indexed via streams, with version tracking and LRU caching.
95
+ description: Per-owner SQLite FTS5 indexes stored on S3, queried from Lambda /tmp
96
+ with version tracking.
83
97
  email:
84
98
  - adam@stowzilla.com
85
99
  executables: []
@@ -90,13 +104,22 @@ files:
90
104
  - LICENSE.txt
91
105
  - README.md
92
106
  - certs/stowzilla.pem
107
+ - infrastructure/routes.tf.rb
108
+ - infrastructure/schema.tf.rb
109
+ - lambda/controllers/s3arch_controller.rb
93
110
  - lib/s3arch.rb
94
111
  - lib/s3arch/configuration.rb
112
+ - lib/s3arch/dashboard.rb
113
+ - lib/s3arch/dashboard/application.rb
114
+ - lib/s3arch/dashboard/views/index.html.erb
95
115
  - lib/s3arch/handler.rb
116
+ - lib/s3arch/holster.rb
96
117
  - lib/s3arch/indexer.rb
118
+ - lib/s3arch/routes.rb
97
119
  - lib/s3arch/searcher.rb
98
120
  - lib/s3arch/tokenizer.rb
99
121
  - lib/s3arch/version.rb
122
+ - lib/s3arch/web.rb
100
123
  homepage: https://github.com/stowzilla/s3arch
101
124
  licenses:
102
125
  - MIT
metadata.gz.sig CHANGED
Binary file