axhub-sdk 0.4.0 → 0.6.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.
- checksums.yaml +4 -4
- data/README.md +2 -14
- data/lib/axhub_sdk/version.rb +1 -1
- data/lib/axhub_sdk.rb +96 -22
- metadata +2 -13
- data/lib/axhub_sdk/data/client.rb +0 -250
- data/lib/axhub_sdk/data/discover.rb +0 -148
- data/lib/axhub_sdk/data/dsl/ops.rb +0 -150
- data/lib/axhub_sdk/data/dsl/schema.rb +0 -66
- data/lib/axhub_sdk/data/dsl/validation.rb +0 -60
- data/lib/axhub_sdk/data/errors.rb +0 -68
- data/lib/axhub_sdk/data/pagination.rb +0 -100
- data/lib/axhub_sdk/data/projection.rb +0 -50
- data/lib/axhub_sdk/data/schema_cache.rb +0 -105
- data/lib/axhub_sdk/data/where_serializer.rb +0 -84
- data/lib/axhub_sdk/data.rb +0 -67
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module AxHub
|
|
4
|
-
module Data
|
|
5
|
-
# Typed data-layer errors. These subclass the existing single AxHub::Error so
|
|
6
|
-
# the (category, code) contract that the conformance vectors + error tests
|
|
7
|
-
# match on keeps working, while callers can still rescue the specific failure
|
|
8
|
-
# (mirrors node/python LegacyCursorError / InvalidCursorError / ValidationError
|
|
9
|
-
# / TableNotFoundError / IntrospectFailedError / ScanLimitExceededError).
|
|
10
|
-
class ValidationError < AxHub::Error
|
|
11
|
-
def initialize(message, code = 'validation', status: 0, retryable: false, request_id: nil)
|
|
12
|
-
super(category: 'validation', code: code, message: message, status: status, retryable: retryable, request_id: request_id)
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Raised when an after/before keyset or v1:/v2: cursor token is supplied; the
|
|
17
|
-
# live AX Hub data API is offset-only (mirrors node LegacyCursorError).
|
|
18
|
-
class LegacyCursorError < AxHub::Error
|
|
19
|
-
def initialize(message, request_id: nil)
|
|
20
|
-
super(category: 'validation', code: 'legacy_cursor', message: message, status: 0, retryable: false, request_id: request_id)
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
class InvalidCursorError < AxHub::Error
|
|
25
|
-
def initialize(message, request_id: nil)
|
|
26
|
-
super(category: 'validation', code: 'invalid_cursor', message: message, status: 0, retryable: false, request_id: request_id)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
class TableNotFoundError < AxHub::Error
|
|
31
|
-
def initialize(message, request_id: nil)
|
|
32
|
-
super(category: 'not_found', code: 'table_not_found', message: message, status: 404, retryable: false, request_id: request_id)
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
class IntrospectFailedError < AxHub::Error
|
|
37
|
-
def initialize(message, status: 0, retryable: false, request_id: nil)
|
|
38
|
-
super(category: 'internal', code: 'introspect_failed', message: message, status: status, retryable: retryable, request_id: request_id)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
class ScanLimitExceededError < AxHub::Error
|
|
43
|
-
def initialize(message, request_id: nil)
|
|
44
|
-
super(category: 'internal', code: 'scan_limit_exceeded', message: message, status: 0, retryable: false, request_id: request_id)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
# The backend 400s an unfiltered list/count on NON-owner-scoped tables
|
|
48
|
-
# ("최소 1개의 WHERE 필터가 필요해요") but ACCEPTS it on owner-scoped tables
|
|
49
|
-
# (rows auto-scope to the caller) — both confirmed live 2026-06. A client
|
|
50
|
-
# pre-check cannot tell them apart (0.3.0 regression), so the request goes
|
|
51
|
-
# through and only the backend 400 is normalized.
|
|
52
|
-
def self.map_where_required(op)
|
|
53
|
-
yield
|
|
54
|
-
rescue StandardError => e
|
|
55
|
-
code = e.respond_to?(:code) ? e.code : nil
|
|
56
|
-
status = e.respond_to?(:status) ? e.status : nil
|
|
57
|
-
if code.to_s == 'required' && status.to_i == 400
|
|
58
|
-
raise ValidationError.new(
|
|
59
|
-
"AxHub data #{op} requires at least one WHERE filter on this table " \
|
|
60
|
-
'(the backend rejects unfiltered scans on non-owner-scoped tables). Pass `where:`.',
|
|
61
|
-
'where_required'
|
|
62
|
-
)
|
|
63
|
-
end
|
|
64
|
-
raise
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
end
|
|
68
|
-
end
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module AxHub
|
|
4
|
-
module Data
|
|
5
|
-
# Offset pagination helpers (subset of node core pagination that the data
|
|
6
|
-
# ergonomic layer depends on).
|
|
7
|
-
#
|
|
8
|
-
# Ported: serialize_order_by / normalize_order_by, is_v2_cursor,
|
|
9
|
-
# MAX_CURSOR_TOKEN_LENGTH, list_all, and the PaginatedList / ListAllItem
|
|
10
|
-
# result shapes. Keyset encode/decode is intentionally NOT ported: the live
|
|
11
|
-
# AX Hub data API is offset-only, so the data layer only needs the order-by
|
|
12
|
-
# normalizer and the cursor-shape guards used to reject legacy keyset tokens.
|
|
13
|
-
MAX_CURSOR_TOKEN_LENGTH = 4096
|
|
14
|
-
|
|
15
|
-
PaginatedList = Struct.new(
|
|
16
|
-
:items, :next_cursor, :first_cursor, :has_next, :has_prev, :total, :total_is_exact,
|
|
17
|
-
keyword_init: true
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
# Either an item (type == :item) or a drift marker (type == :drift) when the
|
|
21
|
-
# backend total grows mid-scan.
|
|
22
|
-
ListAllItem = Struct.new(:type, :value, :added_since, keyword_init: true) do
|
|
23
|
-
def initialize(type:, value: nil, added_since: 0)
|
|
24
|
-
super(type: type, value: value, added_since: added_since)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
module_function
|
|
29
|
-
|
|
30
|
-
# order_by = String | Array<{ field: String, dir?: "asc"|"desc" }>
|
|
31
|
-
def normalize_order_by(order_by)
|
|
32
|
-
if order_by.is_a?(String)
|
|
33
|
-
fields = []
|
|
34
|
-
order_by.split(',').each do |part|
|
|
35
|
-
trimmed = part.strip
|
|
36
|
-
f = if trimmed.start_with?('-')
|
|
37
|
-
{ 'field' => trimmed[1..], 'dir' => 'desc' }
|
|
38
|
-
elsif trimmed.start_with?('+')
|
|
39
|
-
{ 'field' => trimmed[1..], 'dir' => 'asc' }
|
|
40
|
-
else
|
|
41
|
-
{ 'field' => trimmed, 'dir' => 'asc' }
|
|
42
|
-
end
|
|
43
|
-
fields << f unless f['field'].nil? || f['field'].empty?
|
|
44
|
-
end
|
|
45
|
-
elsif order_by && !order_by.empty?
|
|
46
|
-
fields = order_by.map do |p|
|
|
47
|
-
h = p.transform_keys(&:to_s)
|
|
48
|
-
{ 'field' => h['field'], 'dir' => h.fetch('dir', 'asc') }
|
|
49
|
-
end
|
|
50
|
-
else
|
|
51
|
-
fields = []
|
|
52
|
-
end
|
|
53
|
-
if !fields.empty? && fields.none? { |f| f['field'] == 'id' }
|
|
54
|
-
fields << { 'field' => 'id', 'dir' => 'asc' }
|
|
55
|
-
end
|
|
56
|
-
fields
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def serialize_order_by(order_by)
|
|
60
|
-
normalized = normalize_order_by(order_by)
|
|
61
|
-
return (order_by.is_a?(String) ? order_by : nil) if normalized.empty?
|
|
62
|
-
|
|
63
|
-
normalized.map { |f| "#{f['dir'] == 'desc' ? '-' : ''}#{f['field']}" }.join(',')
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def is_v2_cursor(token)
|
|
67
|
-
token.is_a?(String) && token.start_with?('v2:')
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Drive a paginated fetcher to exhaustion, yielding each item and a drift
|
|
71
|
-
# marker when the backend total grows mid-iteration (mirrors node listAll).
|
|
72
|
-
# Returns an Enumerator when no block is given (idiomatic Ruby).
|
|
73
|
-
def list_all(fetcher, opts = {})
|
|
74
|
-
return enum_for(:list_all, fetcher, opts) unless block_given?
|
|
75
|
-
|
|
76
|
-
cursor = opts[:cursor]
|
|
77
|
-
initial_total = nil
|
|
78
|
-
last_total = nil
|
|
79
|
-
loop do
|
|
80
|
-
page = fetcher.call(page_size: opts[:page_size], cursor: cursor)
|
|
81
|
-
unless page.total.nil?
|
|
82
|
-
if initial_total.nil?
|
|
83
|
-
initial_total = page.total
|
|
84
|
-
last_total = page.total
|
|
85
|
-
else
|
|
86
|
-
base = last_total.nil? ? initial_total : last_total
|
|
87
|
-
if page.total > base
|
|
88
|
-
yield ListAllItem.new(type: :drift, added_since: page.total - base)
|
|
89
|
-
last_total = page.total
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
page.items.each { |item| yield ListAllItem.new(type: :item, value: item) }
|
|
94
|
-
return if page.next_cursor.nil?
|
|
95
|
-
|
|
96
|
-
cursor = page.next_cursor
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'errors'
|
|
4
|
-
|
|
5
|
-
module AxHub
|
|
6
|
-
module Data
|
|
7
|
-
# Column projection: `select` serialization, validation, and client-side row
|
|
8
|
-
# narrowing (mirrors node/python projection).
|
|
9
|
-
#
|
|
10
|
-
# serialize_select joins columns with commas into the `_select` query param.
|
|
11
|
-
# validate_select_columns rejects an empty select and, when a schema is known,
|
|
12
|
-
# unknown columns. project_row/project_rows narrow returned rows to the
|
|
13
|
-
# selected keys client-side. Rows are string-keyed (camelize-off transport).
|
|
14
|
-
module_function
|
|
15
|
-
|
|
16
|
-
def serialize_select(select)
|
|
17
|
-
return nil if select.nil?
|
|
18
|
-
|
|
19
|
-
select.join(',')
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def validate_select_columns(schema, select)
|
|
23
|
-
return if select.nil?
|
|
24
|
-
|
|
25
|
-
if select.length.zero?
|
|
26
|
-
raise ValidationError.new('select must include at least one column; omit select to fetch full rows', 'select_empty')
|
|
27
|
-
end
|
|
28
|
-
return if schema.nil?
|
|
29
|
-
|
|
30
|
-
allowed = schema.columns.keys
|
|
31
|
-
invalid = select.reject { |c| allowed.include?(c) }
|
|
32
|
-
return if invalid.empty?
|
|
33
|
-
|
|
34
|
-
plural = invalid.length == 1 ? '' : 's'
|
|
35
|
-
raise ValidationError.new("select contains unknown column#{plural}: #{invalid.join(', ')}", 'select_unknown_column')
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def project_row(row, select)
|
|
39
|
-
return row.dup if select.nil?
|
|
40
|
-
|
|
41
|
-
select.each_with_object({}) { |k, acc| acc[k] = row[k] if row.key?(k) }
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def project_rows(rows, select)
|
|
45
|
-
return rows.map(&:dup) if select.nil?
|
|
46
|
-
|
|
47
|
-
rows.map { |r| project_row(r, select) }
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'dsl/schema'
|
|
4
|
-
|
|
5
|
-
module AxHub
|
|
6
|
-
module Data
|
|
7
|
-
# Per-client schema cache for runtime data.discover (mirrors node/python
|
|
8
|
-
# schema-cache).
|
|
9
|
-
#
|
|
10
|
-
# Uses an insertion-ordered Ruby Hash for deterministic LRU eviction
|
|
11
|
-
# (delete+reinsert = move-to-end on read/write, `shift` = evict oldest) and a
|
|
12
|
-
# negative-TTL stale-while-error window: a transient 5xx during refresh keeps
|
|
13
|
-
# the previous entry alive briefly instead of evicting it. The node version
|
|
14
|
-
# de-dupes concurrent in-flight loads; the sync Ruby port omits the in-flight
|
|
15
|
-
# map (no concurrency within a single synchronous call).
|
|
16
|
-
DEFAULT_SCHEMA_CACHE_TTL_MS = 5 * 60_000
|
|
17
|
-
DEFAULT_SCHEMA_CACHE_MAX_ENTRIES = 1000
|
|
18
|
-
DEFAULT_SCHEMA_CACHE_NEGATIVE_TTL_MS = 30_000
|
|
19
|
-
|
|
20
|
-
module_function
|
|
21
|
-
|
|
22
|
-
def schema_cache_key(tenant_slug, app_slug, table)
|
|
23
|
-
"#{tenant_slug}/#{app_slug}/#{table}"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
class SchemaCache
|
|
27
|
-
Entry = Struct.new(:schema, :expires_at, keyword_init: true)
|
|
28
|
-
|
|
29
|
-
def initialize(max_entries: nil, ttl_ms: nil, negative_ttl_ms: nil)
|
|
30
|
-
@store = {}
|
|
31
|
-
@max_entries = [1, max_entries.nil? ? DEFAULT_SCHEMA_CACHE_MAX_ENTRIES : max_entries].max
|
|
32
|
-
@ttl_ms = [1, ttl_ms.nil? ? DEFAULT_SCHEMA_CACHE_TTL_MS : ttl_ms].max
|
|
33
|
-
@negative_ttl_ms = [0, negative_ttl_ms.nil? ? DEFAULT_SCHEMA_CACHE_NEGATIVE_TTL_MS : negative_ttl_ms].max
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def size
|
|
37
|
-
@store.size
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def get(key)
|
|
41
|
-
entry = @store[key]
|
|
42
|
-
return nil if entry.nil?
|
|
43
|
-
|
|
44
|
-
if entry.expires_at <= _now_ms
|
|
45
|
-
@store.delete(key)
|
|
46
|
-
return nil
|
|
47
|
-
end
|
|
48
|
-
# refresh recency: move to end (delete + reinsert)
|
|
49
|
-
@store.delete(key)
|
|
50
|
-
@store[key] = entry
|
|
51
|
-
entry.schema
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def set(key, schema, ttl_ms = nil)
|
|
55
|
-
@store.delete(key)
|
|
56
|
-
@store[key] = Entry.new(schema: schema, expires_at: _now_ms + [1, ttl_ms.nil? ? @ttl_ms : ttl_ms].max)
|
|
57
|
-
_evict_overflow
|
|
58
|
-
nil
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def invalidate(key = nil)
|
|
62
|
-
if key.nil?
|
|
63
|
-
@store.clear
|
|
64
|
-
else
|
|
65
|
-
@store.delete(key)
|
|
66
|
-
end
|
|
67
|
-
nil
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def get_or_set(key, fresh: nil, ttl_ms: nil)
|
|
71
|
-
unless fresh
|
|
72
|
-
cached = get(key)
|
|
73
|
-
return cached unless cached.nil?
|
|
74
|
-
end
|
|
75
|
-
previous = @store[key]
|
|
76
|
-
begin
|
|
77
|
-
schema = yield
|
|
78
|
-
rescue StandardError => e
|
|
79
|
-
if !previous.nil? && @negative_ttl_ms.positive? && _transient_server_error?(e)
|
|
80
|
-
@store.delete(key)
|
|
81
|
-
@store[key] = Entry.new(schema: previous.schema, expires_at: _now_ms + @negative_ttl_ms)
|
|
82
|
-
end
|
|
83
|
-
raise
|
|
84
|
-
end
|
|
85
|
-
set(key, schema, ttl_ms)
|
|
86
|
-
schema
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
private
|
|
90
|
-
|
|
91
|
-
def _now_ms
|
|
92
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000.0
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def _transient_server_error?(err)
|
|
96
|
-
status = err.respond_to?(:status) ? err.status : nil
|
|
97
|
-
status.is_a?(Integer) && status >= 500
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def _evict_overflow
|
|
101
|
-
@store.shift while @store.size > @max_entries # oldest first
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'json'
|
|
4
|
-
require 'date'
|
|
5
|
-
require_relative 'errors'
|
|
6
|
-
|
|
7
|
-
module AxHub
|
|
8
|
-
module Data
|
|
9
|
-
# Serialize the predicate DSL into backend filter query params (mirrors node
|
|
10
|
-
# where-serializer + python where_serializer).
|
|
11
|
-
#
|
|
12
|
-
# Each pushable atom becomes column=<op>.<value> (PostgREST-style). Repeated
|
|
13
|
-
# columns collapse into an array so the transport emits repeated query params
|
|
14
|
-
# (URI.encode_www_form repeats array-valued keys). Only top-level and(...) of
|
|
15
|
-
# pushable atoms and bare atoms are accepted; or/not/raw and nested-and raise
|
|
16
|
-
# ValidationError — this matches the live backend's filter grammar.
|
|
17
|
-
PUSHABLE_BINARY = %i[eq ne gt gte lt lte like].freeze
|
|
18
|
-
|
|
19
|
-
module_function
|
|
20
|
-
|
|
21
|
-
def serialize_where(expr)
|
|
22
|
-
return {} if expr.nil?
|
|
23
|
-
|
|
24
|
-
out = {}
|
|
25
|
-
_collect_pushable_filters(expr, allow_and: true).each do |f|
|
|
26
|
-
_append_query(out, f[:column], f[:value])
|
|
27
|
-
end
|
|
28
|
-
out
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def _append_query(out, key, value)
|
|
32
|
-
if !out.key?(key)
|
|
33
|
-
out[key] = value
|
|
34
|
-
elsif out[key].is_a?(Array)
|
|
35
|
-
out[key] << value
|
|
36
|
-
else
|
|
37
|
-
out[key] = [out[key], value]
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def _collect_pushable_filters(expr, allow_and:)
|
|
42
|
-
op = expr[:op]
|
|
43
|
-
if PUSHABLE_BINARY.include?(op)
|
|
44
|
-
return [{ column: expr[:column], value: "#{op}.#{_stringify(expr[:value])}" }]
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
if op == :in
|
|
48
|
-
values = expr[:values].map { |v| _stringify(v) }
|
|
49
|
-
bad = values.find { |v| v.include?(',') }
|
|
50
|
-
unless bad.nil?
|
|
51
|
-
raise ValidationError.new(
|
|
52
|
-
"IN filter values cannot contain commas because the live backend uses comma-separated IN lists (bad value: #{bad})",
|
|
53
|
-
'filter_in_comma'
|
|
54
|
-
)
|
|
55
|
-
end
|
|
56
|
-
return [{ column: expr[:column], value: "in.#{values.join(',')}" }]
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
if op == :and && allow_and
|
|
60
|
-
out = []
|
|
61
|
-
expr[:clauses].each { |clause| out.concat(_collect_pushable_filters(clause, allow_and: false)) }
|
|
62
|
-
return out
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# or / not / raw / nested-and all fall through to the rejection below.
|
|
66
|
-
raise ValidationError.new(
|
|
67
|
-
"Data where clause '#{op}' cannot be pushed to the live backend; use top-level and(eq/ne/gt/gte/lt/lte/in/like) only",
|
|
68
|
-
'unsupported_filter'
|
|
69
|
-
)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def _stringify(value)
|
|
73
|
-
case value
|
|
74
|
-
when Time then value.iso8601
|
|
75
|
-
when DateTime, Date then value.iso8601
|
|
76
|
-
when nil then 'null'
|
|
77
|
-
when true then 'true'
|
|
78
|
-
when false then 'false'
|
|
79
|
-
when String, Integer, Float then value.to_s
|
|
80
|
-
else JSON.generate(value)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
data/lib/axhub_sdk/data.rb
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Ergonomic data layer for the AX Hub Ruby SDK.
|
|
4
|
-
#
|
|
5
|
-
# Public surface (mirrors the node `resources/data` layer):
|
|
6
|
-
#
|
|
7
|
-
# client.tenant(tenant_slug).app(app_slug).data.table(name_or_schema)
|
|
8
|
-
# client.tenant(tenant_slug).app(app_slug).data.discover(table)
|
|
9
|
-
#
|
|
10
|
-
# returns a DataTableClient with list / list_all / count / get / insert /
|
|
11
|
-
# insert_many / update / delete, plus the predicate DSL (where(col).eq(v) /
|
|
12
|
-
# and_(...) / block form), define_schema(...), and offset-only pagination.
|
|
13
|
-
require_relative 'data/errors'
|
|
14
|
-
require_relative 'data/dsl/schema'
|
|
15
|
-
require_relative 'data/dsl/ops'
|
|
16
|
-
require_relative 'data/dsl/validation'
|
|
17
|
-
require_relative 'data/pagination'
|
|
18
|
-
require_relative 'data/projection'
|
|
19
|
-
require_relative 'data/where_serializer'
|
|
20
|
-
require_relative 'data/schema_cache'
|
|
21
|
-
require_relative 'data/discover'
|
|
22
|
-
require_relative 'data/client'
|
|
23
|
-
|
|
24
|
-
module AxHub
|
|
25
|
-
module Data
|
|
26
|
-
# Fluent scope wrappers so the public chain reads `client.tenant(t).app(a).data`
|
|
27
|
-
# (mirrors node/python, where `.data` on the app scope yields the ergonomic
|
|
28
|
-
# AppDataFactory). `client.data` itself stays the operation-id OperationClient.
|
|
29
|
-
class AppScope
|
|
30
|
-
attr_reader :data
|
|
31
|
-
|
|
32
|
-
def initialize(ergo_data, tenant_slug, app_slug)
|
|
33
|
-
# `ergo_data` is the single per-client DataClient (memoized on Client), so
|
|
34
|
-
# its schema cache persists across every tenant().app() chain — node parity.
|
|
35
|
-
@data = ergo_data.scoped(tenant_slug).app(app_slug)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
class TenantScope
|
|
40
|
-
def initialize(ergo_data, tenant_slug)
|
|
41
|
-
@ergo_data = ergo_data
|
|
42
|
-
@tenant_slug = tenant_slug
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def app(app_slug)
|
|
46
|
-
AppScope.new(@ergo_data, @tenant_slug, app_slug)
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# --- Ergonomic data layer fluent surface (mirrors node client.tenant().app().data) ---
|
|
52
|
-
# `client.data` stays the operation-id route-table OperationClient (the
|
|
53
|
-
# conformance vectors + e2e tests depend on it). The ergonomic data layer is
|
|
54
|
-
# reached only through the tenant/app fluent chain, exactly as in node/python.
|
|
55
|
-
class Client
|
|
56
|
-
# The single per-client ergonomic DataClient, lazily memoized so the schema
|
|
57
|
-
# cache (TTL/negative-TTL/LRU) survives across tenant().app() chains (mirrors
|
|
58
|
-
# node, where `data` is one per-SDK DataClient).
|
|
59
|
-
def ergo_data
|
|
60
|
-
@ergo_data ||= AxHub::Data::DataClient.new(self, schema_cache: @schema_cache_opt)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def tenant(tenant_slug)
|
|
64
|
-
AxHub::Data::TenantScope.new(ergo_data, tenant_slug)
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|