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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +32 -0
- data/infrastructure/routes.tf.rb +8 -0
- data/infrastructure/schema.tf.rb +6 -0
- data/lambda/controllers/s3arch_controller.rb +66 -0
- data/lib/s3arch/configuration.rb +9 -0
- data/lib/s3arch/dashboard/application.rb +86 -0
- data/lib/s3arch/dashboard/views/index.html.erb +150 -0
- data/lib/s3arch/dashboard.rb +13 -0
- data/lib/s3arch/holster.rb +7 -0
- data/lib/s3arch/indexer.rb +22 -13
- data/lib/s3arch/routes.rb +14 -0
- data/lib/s3arch/version.rb +1 -1
- data/lib/s3arch/web.rb +26 -0
- data/lib/s3arch.rb +5 -0
- data.tar.gz.sig +0 -0
- metadata +26 -3
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6e0ba51260081a3de075c6e14332909892693b64dce6eb1a3e52f7b1f145b537
|
|
4
|
+
data.tar.gz: 7553e90741ef8a1cc6c72b55bb9239c0e238da2a09916d34e8edf18780942a24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,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
|
data/lib/s3arch/configuration.rb
CHANGED
|
@@ -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>
|
data/lib/s3arch/indexer.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
272
|
-
@config.
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
278
|
-
|
|
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
|
data/lib/s3arch/version.rb
CHANGED
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.
|
|
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
|
-
|
|
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
|