claude-agent-framework 1.0.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 (111) hide show
  1. package/README.md +128 -0
  2. package/bin/claude-framework +3 -0
  3. package/framework/agents/design-lead.md +240 -0
  4. package/framework/agents/product-owner.md +179 -0
  5. package/framework/agents/tech-lead.md +226 -0
  6. package/framework/commands/ayuda.md +127 -0
  7. package/framework/commands/a/303/261adir.md +98 -0
  8. package/framework/commands/backup.md +397 -0
  9. package/framework/commands/cambiar.md +110 -0
  10. package/framework/commands/cloud.md +457 -0
  11. package/framework/commands/code.md +142 -0
  12. package/framework/commands/debug.md +334 -0
  13. package/framework/commands/deploy.md +383 -0
  14. package/framework/commands/deshacer.md +120 -0
  15. package/framework/commands/estado.md +218 -0
  16. package/framework/commands/explica.md +227 -0
  17. package/framework/commands/feature.md +120 -0
  18. package/framework/commands/git.md +427 -0
  19. package/framework/commands/historial.md +202 -0
  20. package/framework/commands/learn.md +408 -0
  21. package/framework/commands/movil.md +245 -0
  22. package/framework/commands/nuevo.md +118 -0
  23. package/framework/commands/plan.md +134 -0
  24. package/framework/commands/prd.md +113 -0
  25. package/framework/commands/probar.md +148 -0
  26. package/framework/commands/revisar.md +208 -0
  27. package/framework/commands/seeds.md +230 -0
  28. package/framework/commands/seguridad.md +226 -0
  29. package/framework/commands/tasks.md +157 -0
  30. package/framework/skills/architecture/algorithms.md +970 -0
  31. package/framework/skills/architecture/clean-code.md +1080 -0
  32. package/framework/skills/architecture/design-patterns.md +1984 -0
  33. package/framework/skills/architecture/functional-programming.md +972 -0
  34. package/framework/skills/architecture/solid.md +991 -0
  35. package/framework/skills/cloud/cloud-aws.md +848 -0
  36. package/framework/skills/cloud/cloud-azure.md +931 -0
  37. package/framework/skills/cloud/cloud-gcp.md +848 -0
  38. package/framework/skills/cloud/message-queues.md +1229 -0
  39. package/framework/skills/core/accessibility.md +401 -0
  40. package/framework/skills/core/api.md +474 -0
  41. package/framework/skills/core/authentication.md +306 -0
  42. package/framework/skills/core/authorization.md +388 -0
  43. package/framework/skills/core/background-jobs.md +341 -0
  44. package/framework/skills/core/caching.md +473 -0
  45. package/framework/skills/core/code-review.md +341 -0
  46. package/framework/skills/core/controllers.md +290 -0
  47. package/framework/skills/core/cua.md +285 -0
  48. package/framework/skills/core/documentation.md +472 -0
  49. package/framework/skills/core/file-uploads.md +351 -0
  50. package/framework/skills/core/hotwire-native.md +296 -0
  51. package/framework/skills/core/hotwire.md +278 -0
  52. package/framework/skills/core/i18n.md +334 -0
  53. package/framework/skills/core/imports-exports.md +750 -0
  54. package/framework/skills/core/infrastructure.md +337 -0
  55. package/framework/skills/core/models.md +228 -0
  56. package/framework/skills/core/notifications.md +672 -0
  57. package/framework/skills/core/payments.md +581 -0
  58. package/framework/skills/core/performance.md +361 -0
  59. package/framework/skills/core/rails-scaffold.md +131 -0
  60. package/framework/skills/core/search.md +518 -0
  61. package/framework/skills/core/security.md +565 -0
  62. package/framework/skills/core/seeds.md +307 -0
  63. package/framework/skills/core/seo.md +542 -0
  64. package/framework/skills/core/testing.md +393 -0
  65. package/framework/skills/core/views.md +260 -0
  66. package/framework/skills/core/websockets.md +564 -0
  67. package/framework/skills/data/advanced-sql.md +1204 -0
  68. package/framework/skills/data/nosql.md +1141 -0
  69. package/framework/skills/devops/containers-advanced.md +1237 -0
  70. package/framework/skills/devops/debugging.md +834 -0
  71. package/framework/skills/devops/git-workflow.md +752 -0
  72. package/framework/skills/devops/networking.md +932 -0
  73. package/framework/skills/devops/shell-scripting.md +1132 -0
  74. package/framework/sub-agents/architecture-patterns-agent.md +1450 -0
  75. package/framework/sub-agents/cloud-agent.md +677 -0
  76. package/framework/sub-agents/data.md +504 -0
  77. package/framework/sub-agents/debugging-agent.md +554 -0
  78. package/framework/sub-agents/devops.md +483 -0
  79. package/framework/sub-agents/docs.md +176 -0
  80. package/framework/sub-agents/frontend-dev.md +349 -0
  81. package/framework/sub-agents/git-workflow-agent.md +697 -0
  82. package/framework/sub-agents/integrations.md +630 -0
  83. package/framework/sub-agents/native-dev.md +434 -0
  84. package/framework/sub-agents/qa.md +138 -0
  85. package/framework/sub-agents/rails-dev.md +375 -0
  86. package/framework/sub-agents/security.md +526 -0
  87. package/framework/sub-agents/ui.md +437 -0
  88. package/framework/sub-agents/ux.md +284 -0
  89. package/framework/templates/api-spec.md +500 -0
  90. package/framework/templates/component-spec.md +248 -0
  91. package/framework/templates/feature.json +13 -0
  92. package/framework/templates/model-spec.md +318 -0
  93. package/framework/templates/prd-template.md +80 -0
  94. package/framework/templates/task-plan.md +122 -0
  95. package/framework/templates/task-user-story.md +52 -0
  96. package/framework/templates/technical-spec.md +260 -0
  97. package/framework/templates/user-story.md +95 -0
  98. package/package.json +42 -0
  99. package/project-templates/CLAUDE.md +42 -0
  100. package/project-templates/contexts/architecture.md +25 -0
  101. package/project-templates/contexts/conventions.md +46 -0
  102. package/project-templates/contexts/design-system.md +47 -0
  103. package/project-templates/contexts/requirements.md +38 -0
  104. package/project-templates/contexts/stack.md +30 -0
  105. package/project-templates/history/active/models.md +11 -0
  106. package/project-templates/history/changelog.md +15 -0
  107. package/project-templates/workspace/.gitkeep +0 -0
  108. package/src/cli.js +52 -0
  109. package/src/init.js +104 -0
  110. package/src/status.js +75 -0
  111. package/src/update.js +88 -0
@@ -0,0 +1,1141 @@
1
+ # Skill: Bases de Datos NoSQL
2
+
3
+ ## Purpose
4
+
5
+ Implementar y utilizar bases de datos NoSQL en aplicaciones Rails para casos de uso especificos donde SQL tradicional no es la mejor opcion.
6
+
7
+ ## Tipos de Bases NoSQL
8
+
9
+ ```markdown
10
+ | Tipo | Ejemplos | Casos de Uso |
11
+ |------|----------|--------------|
12
+ | Document | MongoDB, CouchDB | Datos semi-estructurados, CMS, catalogos |
13
+ | Key-Value | Redis, Memcached | Cache, sessions, colas |
14
+ | Column-Family | Cassandra, HBase | Time-series, analytics a gran escala |
15
+ | Graph | Neo4j, Amazon Neptune | Redes sociales, recomendaciones |
16
+ | Search | Elasticsearch, Solr | Full-text search, logs |
17
+ ```
18
+
19
+ ## MongoDB
20
+
21
+ ### Configuracion con Mongoid
22
+
23
+ ```ruby
24
+ # Gemfile
25
+ gem "mongoid"
26
+
27
+ # Generar configuracion
28
+ # rails g mongoid:config
29
+
30
+ # config/mongoid.yml
31
+ development:
32
+ clients:
33
+ default:
34
+ database: myapp_development
35
+ hosts:
36
+ - localhost:27017
37
+ options:
38
+ server_selection_timeout: 5
39
+ max_pool_size: 5
40
+
41
+ production:
42
+ clients:
43
+ default:
44
+ uri: <%= ENV['MONGODB_URI'] %>
45
+ options:
46
+ ssl: true
47
+ ssl_verify: true
48
+ max_pool_size: 50
49
+ ```
50
+
51
+ ### Modelos con Mongoid
52
+
53
+ ```ruby
54
+ # app/models/product.rb
55
+ class Product
56
+ include Mongoid::Document
57
+ include Mongoid::Timestamps
58
+
59
+ # Campos con tipos
60
+ field :name, type: String
61
+ field :sku, type: String
62
+ field :price, type: BigDecimal
63
+ field :stock_quantity, type: Integer, default: 0
64
+ field :active, type: Boolean, default: true
65
+ field :tags, type: Array, default: []
66
+ field :release_date, type: Date
67
+
68
+ # Documento embebido (almacenado dentro del producto)
69
+ embeds_many :variants
70
+ embeds_one :dimensions
71
+
72
+ # Referencia a otro documento (como foreign key)
73
+ belongs_to :category
74
+ has_many :reviews
75
+
76
+ # Indices
77
+ index({ sku: 1 }, { unique: true })
78
+ index({ name: "text", description: "text" })
79
+ index({ category_id: 1, active: 1 })
80
+ index({ tags: 1 })
81
+
82
+ # Validaciones
83
+ validates :name, presence: true
84
+ validates :sku, presence: true, uniqueness: true
85
+ validates :price, numericality: { greater_than: 0 }
86
+
87
+ # Scopes
88
+ scope :active, -> { where(active: true) }
89
+ scope :in_stock, -> { where(:stock_quantity.gt => 0) }
90
+ scope :by_category, ->(cat_id) { where(category_id: cat_id) }
91
+ scope :with_tag, ->(tag) { where(:tags.in => [tag]) }
92
+
93
+ # Callbacks
94
+ before_save :normalize_sku
95
+
96
+ private
97
+
98
+ def normalize_sku
99
+ self.sku = sku.upcase.strip if sku.present?
100
+ end
101
+ end
102
+
103
+ # app/models/variant.rb (documento embebido)
104
+ class Variant
105
+ include Mongoid::Document
106
+
107
+ embedded_in :product
108
+
109
+ field :name, type: String
110
+ field :sku_suffix, type: String
111
+ field :price_modifier, type: BigDecimal, default: 0
112
+ field :stock_quantity, type: Integer, default: 0
113
+ field :attributes, type: Hash, default: {}
114
+
115
+ validates :name, presence: true
116
+ end
117
+
118
+ # app/models/dimensions.rb
119
+ class Dimensions
120
+ include Mongoid::Document
121
+
122
+ embedded_in :product
123
+
124
+ field :width, type: Float
125
+ field :height, type: Float
126
+ field :depth, type: Float
127
+ field :weight, type: Float
128
+ field :unit, type: String, default: "cm"
129
+ end
130
+ ```
131
+
132
+ ### Queries en MongoDB
133
+
134
+ ```ruby
135
+ # Busquedas basicas
136
+ Product.where(active: true)
137
+ Product.where(price: 10..100)
138
+ Product.where(:stock_quantity.gt => 0)
139
+ Product.where(:tags.in => ["electronics", "gadgets"])
140
+
141
+ # Busqueda en campos anidados
142
+ Product.where("dimensions.weight" => { "$lt" => 5 })
143
+ Product.where("variants.name" => "Large")
144
+
145
+ # Busqueda de texto
146
+ Product.text_search("wireless headphones")
147
+
148
+ # Operadores de comparacion
149
+ Product.where(:price.gt => 50)
150
+ Product.where(:price.gte => 50)
151
+ Product.where(:price.lt => 100)
152
+ Product.where(:price.lte => 100)
153
+ Product.where(:price.ne => 0)
154
+
155
+ # Operadores logicos
156
+ Product.or({ active: true }, { :stock_quantity.gt => 0 })
157
+ Product.and({ active: true }, { :price.lt => 100 })
158
+ Product.not.where(active: false)
159
+
160
+ # Operadores de array
161
+ Product.where(:tags.all => ["featured", "sale"])
162
+ Product.where(:tags.size => 3)
163
+ Product.where(:tags.elem_match => { "$regex" => /^tech/ })
164
+
165
+ # Proyeccion (seleccionar campos)
166
+ Product.only(:name, :price)
167
+ Product.without(:description, :metadata)
168
+
169
+ # Ordenamiento y paginacion
170
+ Product.order_by(price: :asc).skip(20).limit(10)
171
+ Product.order_by(created_at: :desc).first
172
+
173
+ # Aggregation
174
+ Product.collection.aggregate([
175
+ { "$match" => { active: true } },
176
+ { "$group" => {
177
+ "_id" => "$category_id",
178
+ "count" => { "$sum" => 1 },
179
+ "avg_price" => { "$avg" => "$price" }
180
+ }},
181
+ { "$sort" => { "count" => -1 } }
182
+ ])
183
+ ```
184
+
185
+ ### Aggregation Pipeline
186
+
187
+ ```ruby
188
+ # app/services/product_analytics.rb
189
+ class ProductAnalytics
190
+ def self.sales_by_category(start_date:, end_date:)
191
+ Order.collection.aggregate([
192
+ # Filtrar por fecha
193
+ { "$match" => {
194
+ "created_at" => {
195
+ "$gte" => start_date,
196
+ "$lte" => end_date
197
+ }
198
+ }},
199
+
200
+ # Descomponer array de items
201
+ { "$unwind" => "$items" },
202
+
203
+ # Lookup para traer producto
204
+ { "$lookup" => {
205
+ "from" => "products",
206
+ "localField" => "items.product_id",
207
+ "foreignField" => "_id",
208
+ "as" => "product"
209
+ }},
210
+
211
+ # Descomponer resultado del lookup
212
+ { "$unwind" => "$product" },
213
+
214
+ # Agrupar por categoria
215
+ { "$group" => {
216
+ "_id" => "$product.category_id",
217
+ "total_revenue" => {
218
+ "$sum" => { "$multiply" => ["$items.quantity", "$items.price"] }
219
+ },
220
+ "total_units" => { "$sum" => "$items.quantity" },
221
+ "order_count" => { "$sum" => 1 }
222
+ }},
223
+
224
+ # Lookup para nombre de categoria
225
+ { "$lookup" => {
226
+ "from" => "categories",
227
+ "localField" => "_id",
228
+ "foreignField" => "_id",
229
+ "as" => "category"
230
+ }},
231
+
232
+ { "$unwind" => "$category" },
233
+
234
+ # Proyectar resultado final
235
+ { "$project" => {
236
+ "_id" => 0,
237
+ "category_name" => "$category.name",
238
+ "total_revenue" => 1,
239
+ "total_units" => 1,
240
+ "order_count" => 1,
241
+ "avg_order_value" => { "$divide" => ["$total_revenue", "$order_count"] }
242
+ }},
243
+
244
+ # Ordenar
245
+ { "$sort" => { "total_revenue" => -1 } }
246
+ ]).to_a
247
+ end
248
+ end
249
+ ```
250
+
251
+ ## Redis
252
+
253
+ ### Configuracion
254
+
255
+ ```ruby
256
+ # Gemfile
257
+ gem "redis"
258
+ gem "hiredis" # Driver nativo mas rapido
259
+ gem "connection_pool"
260
+
261
+ # config/initializers/redis.rb
262
+ require "connection_pool"
263
+
264
+ REDIS_POOL = ConnectionPool.new(size: 10, timeout: 5) do
265
+ Redis.new(
266
+ url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/0" },
267
+ driver: :hiredis
268
+ )
269
+ end
270
+
271
+ # Helper para acceso
272
+ def redis
273
+ REDIS_POOL.with { |conn| yield conn }
274
+ end
275
+
276
+ # O crear cliente global
277
+ REDIS = Redis.new(url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/0" })
278
+ ```
279
+
280
+ ### Strings
281
+
282
+ ```ruby
283
+ # Set/Get basico
284
+ redis { |r| r.set("user:1:name", "John") }
285
+ redis { |r| r.get("user:1:name") } # => "John"
286
+
287
+ # Con expiracion
288
+ redis { |r| r.setex("session:abc123", 3600, user_data.to_json) } # Expira en 1 hora
289
+
290
+ # Solo si no existe
291
+ redis { |r| r.setnx("lock:order:123", "processing") }
292
+
293
+ # Incrementar/Decrementar
294
+ redis { |r| r.incr("visits:page:home") }
295
+ redis { |r| r.incrby("user:1:points", 10) }
296
+ redis { |r| r.decr("inventory:product:456") }
297
+
298
+ # Multiples operaciones
299
+ redis { |r|
300
+ r.mset("key1", "value1", "key2", "value2")
301
+ r.mget("key1", "key2") # => ["value1", "value2"]
302
+ }
303
+ ```
304
+
305
+ ### Hashes
306
+
307
+ ```ruby
308
+ # Almacenar objeto como hash
309
+ redis { |r|
310
+ r.hset("user:1", {
311
+ "name" => "John",
312
+ "email" => "john@example.com",
313
+ "age" => 30
314
+ })
315
+ }
316
+
317
+ # Obtener campos
318
+ redis { |r| r.hget("user:1", "name") } # => "John"
319
+ redis { |r| r.hgetall("user:1") } # => {"name" => "John", ...}
320
+ redis { |r| r.hmget("user:1", "name", "email") }
321
+
322
+ # Incrementar campo numerico
323
+ redis { |r| r.hincrby("user:1", "login_count", 1) }
324
+
325
+ # Verificar existencia
326
+ redis { |r| r.hexists("user:1", "email") } # => true
327
+
328
+ # Patron para cache de objetos
329
+ class UserCache
330
+ def self.get(user_id)
331
+ data = REDIS.hgetall("user:#{user_id}")
332
+ return nil if data.empty?
333
+
334
+ User.new(data.symbolize_keys)
335
+ end
336
+
337
+ def self.set(user)
338
+ REDIS.hset("user:#{user.id}", user.cache_attributes)
339
+ REDIS.expire("user:#{user.id}", 1.hour.to_i)
340
+ end
341
+
342
+ def self.invalidate(user_id)
343
+ REDIS.del("user:#{user_id}")
344
+ end
345
+ end
346
+ ```
347
+
348
+ ### Lists
349
+
350
+ ```ruby
351
+ # Cola FIFO
352
+ redis { |r| r.lpush("queue:emails", email_data.to_json) } # Agregar al inicio
353
+ redis { |r| r.rpop("queue:emails") } # Sacar del final
354
+
355
+ # Cola LIFO (stack)
356
+ redis { |r| r.lpush("stack:undo", action.to_json) }
357
+ redis { |r| r.lpop("stack:undo") }
358
+
359
+ # Obtener rango
360
+ redis { |r| r.lrange("recent:posts", 0, 9) } # Primeros 10
361
+
362
+ # Bloquear hasta que haya elementos (para workers)
363
+ redis { |r| r.brpop("queue:jobs", timeout: 30) }
364
+
365
+ # Limitar tamano de lista
366
+ redis { |r|
367
+ r.lpush("user:1:notifications", notification.to_json)
368
+ r.ltrim("user:1:notifications", 0, 99) # Mantener solo 100
369
+ }
370
+
371
+ # Timeline/Feed
372
+ class UserFeed
373
+ def self.add_post(user_id, post_id)
374
+ followers = User.find(user_id).follower_ids
375
+
376
+ REDIS.pipelined do |pipe|
377
+ followers.each do |follower_id|
378
+ pipe.lpush("feed:#{follower_id}", post_id)
379
+ pipe.ltrim("feed:#{follower_id}", 0, 999)
380
+ end
381
+ end
382
+ end
383
+
384
+ def self.get_feed(user_id, page: 1, per_page: 20)
385
+ start = (page - 1) * per_page
386
+ stop = start + per_page - 1
387
+
388
+ post_ids = REDIS.lrange("feed:#{user_id}", start, stop)
389
+ Post.where(id: post_ids).order_by_ids(post_ids)
390
+ end
391
+ end
392
+ ```
393
+
394
+ ### Sets
395
+
396
+ ```ruby
397
+ # Agregar a set
398
+ redis { |r| r.sadd("tags:product:1", ["electronics", "gadgets", "tech"]) }
399
+
400
+ # Verificar pertenencia
401
+ redis { |r| r.sismember("tags:product:1", "electronics") } # => true
402
+
403
+ # Obtener todos los miembros
404
+ redis { |r| r.smembers("tags:product:1") }
405
+
406
+ # Operaciones de conjuntos
407
+ redis { |r| r.sinter("tags:product:1", "tags:product:2") } # Interseccion
408
+ redis { |r| r.sunion("tags:product:1", "tags:product:2") } # Union
409
+ redis { |r| r.sdiff("tags:product:1", "tags:product:2") } # Diferencia
410
+
411
+ # Contar miembros
412
+ redis { |r| r.scard("online:users") }
413
+
414
+ # Usuarios online
415
+ class OnlineTracker
416
+ def self.mark_online(user_id)
417
+ REDIS.sadd("online:users", user_id)
418
+ REDIS.expire("online:users", 5.minutes.to_i)
419
+ end
420
+
421
+ def self.online_count
422
+ REDIS.scard("online:users")
423
+ end
424
+
425
+ def self.is_online?(user_id)
426
+ REDIS.sismember("online:users", user_id)
427
+ end
428
+
429
+ def self.online_friends(user_id)
430
+ friend_ids = User.find(user_id).friend_ids
431
+ REDIS.sinter("online:users", friend_ids)
432
+ end
433
+ end
434
+ ```
435
+
436
+ ### Sorted Sets
437
+
438
+ ```ruby
439
+ # Agregar con score
440
+ redis { |r| r.zadd("leaderboard", 1000, "user:1") }
441
+ redis { |r| r.zadd("leaderboard", 850, "user:2") }
442
+
443
+ # Incrementar score
444
+ redis { |r| r.zincrby("leaderboard", 50, "user:1") }
445
+
446
+ # Ranking (mayor a menor)
447
+ redis { |r| r.zrevrange("leaderboard", 0, 9, with_scores: true) }
448
+
449
+ # Obtener rank de usuario
450
+ redis { |r| r.zrevrank("leaderboard", "user:1") } # 0-based
451
+
452
+ # Rango por score
453
+ redis { |r| r.zrangebyscore("leaderboard", 500, 1000) }
454
+
455
+ # Leaderboard completo
456
+ class Leaderboard
457
+ def initialize(name)
458
+ @name = "leaderboard:#{name}"
459
+ end
460
+
461
+ def add_score(user_id, score)
462
+ REDIS.zincrby(@name, score, user_id)
463
+ end
464
+
465
+ def top(count = 10)
466
+ REDIS.zrevrange(@name, 0, count - 1, with_scores: true).map do |user_id, score|
467
+ { user_id: user_id, score: score.to_i }
468
+ end
469
+ end
470
+
471
+ def rank_for(user_id)
472
+ rank = REDIS.zrevrank(@name, user_id)
473
+ rank ? rank + 1 : nil
474
+ end
475
+
476
+ def score_for(user_id)
477
+ REDIS.zscore(@name, user_id)&.to_i
478
+ end
479
+
480
+ def around_user(user_id, range: 5)
481
+ rank = REDIS.zrevrank(@name, user_id)
482
+ return [] unless rank
483
+
484
+ start = [0, rank - range].max
485
+ stop = rank + range
486
+
487
+ REDIS.zrevrange(@name, start, stop, with_scores: true).map.with_index do |(uid, score), idx|
488
+ { rank: start + idx + 1, user_id: uid, score: score.to_i }
489
+ end
490
+ end
491
+ end
492
+ ```
493
+
494
+ ### Pub/Sub
495
+
496
+ ```ruby
497
+ # Publicador
498
+ class EventPublisher
499
+ def self.publish(channel, event)
500
+ REDIS.publish("events:#{channel}", event.to_json)
501
+ end
502
+ end
503
+
504
+ # Suscriptor (en proceso separado)
505
+ class EventSubscriber
506
+ def self.subscribe(*channels)
507
+ redis = Redis.new(url: ENV["REDIS_URL"])
508
+
509
+ redis.subscribe(*channels.map { |c| "events:#{c}" }) do |on|
510
+ on.subscribe do |channel, subscriptions|
511
+ puts "Subscribed to #{channel} (#{subscriptions} subscriptions)"
512
+ end
513
+
514
+ on.message do |channel, message|
515
+ event = JSON.parse(message)
516
+ handle_event(channel, event)
517
+ end
518
+ end
519
+ end
520
+
521
+ def self.handle_event(channel, event)
522
+ case channel
523
+ when "events:orders"
524
+ OrderEventHandler.process(event)
525
+ when "events:users"
526
+ UserEventHandler.process(event)
527
+ end
528
+ end
529
+ end
530
+
531
+ # Real-time notifications con Action Cable
532
+ class NotificationChannel < ApplicationCable::Channel
533
+ def subscribed
534
+ # Suscribirse a Redis channel
535
+ redis_subscribe("notifications:#{current_user.id}")
536
+ end
537
+
538
+ private
539
+
540
+ def redis_subscribe(channel)
541
+ Thread.new do
542
+ Redis.new.subscribe(channel) do |on|
543
+ on.message do |_channel, message|
544
+ transmit(JSON.parse(message))
545
+ end
546
+ end
547
+ end
548
+ end
549
+ end
550
+ ```
551
+
552
+ ## Neo4j
553
+
554
+ ### Configuracion
555
+
556
+ ```ruby
557
+ # Gemfile
558
+ gem "neo4j"
559
+ gem "neo4j-core"
560
+
561
+ # config/neo4j.yml
562
+ development:
563
+ type: bolt
564
+ url: bolt://localhost:7687
565
+ username: neo4j
566
+ password: password
567
+
568
+ production:
569
+ type: bolt
570
+ url: <%= ENV['NEO4J_URL'] %>
571
+ username: <%= ENV['NEO4J_USERNAME'] %>
572
+ password: <%= ENV['NEO4J_PASSWORD'] %>
573
+ ```
574
+
575
+ ### Modelos
576
+
577
+ ```ruby
578
+ # app/models/person.rb
579
+ class Person
580
+ include Neo4j::ActiveNode
581
+
582
+ property :name, type: String
583
+ property :email, type: String
584
+ property :born, type: Integer
585
+
586
+ has_many :out, :friends, rel_class: :Friendship, model_class: :Person
587
+ has_many :in, :followers, rel_class: :Follows, model_class: :Person
588
+ has_many :out, :following, rel_class: :Follows, model_class: :Person
589
+ has_many :out, :posts, type: :AUTHORED
590
+ has_many :out, :liked_posts, rel_class: :Likes, model_class: :Post
591
+
592
+ validates :name, presence: true
593
+ validates :email, presence: true, uniqueness: true
594
+ end
595
+
596
+ # app/models/post.rb
597
+ class Post
598
+ include Neo4j::ActiveNode
599
+
600
+ property :title, type: String
601
+ property :body, type: String
602
+ property :created_at, type: DateTime
603
+
604
+ has_one :in, :author, type: :AUTHORED, model_class: :Person
605
+ has_many :in, :likers, rel_class: :Likes, model_class: :Person
606
+ has_many :out, :tags, type: :TAGGED_WITH
607
+ end
608
+
609
+ # app/models/friendship.rb (relacion con propiedades)
610
+ class Friendship
611
+ include Neo4j::ActiveRel
612
+
613
+ from_class :Person
614
+ to_class :Person
615
+ type :FRIENDS_WITH
616
+
617
+ property :since, type: Date
618
+ property :closeness, type: Integer # 1-10
619
+
620
+ validates :since, presence: true
621
+ end
622
+ ```
623
+
624
+ ### Cypher Queries
625
+
626
+ ```ruby
627
+ # Encontrar amigos de amigos
628
+ def friends_of_friends(person)
629
+ Neo4j::ActiveBase.new_query
630
+ .match("(p:Person)-[:FRIENDS_WITH]->(friend)-[:FRIENDS_WITH]->(fof:Person)")
631
+ .where(p: { uuid: person.uuid })
632
+ .where_not("(p)-[:FRIENDS_WITH]->(fof)")
633
+ .where_not("p = fof")
634
+ .return("DISTINCT fof")
635
+ .to_a
636
+ end
637
+
638
+ # Camino mas corto entre dos personas
639
+ def shortest_path(person1, person2)
640
+ Neo4j::ActiveBase.new_query
641
+ .match("path = shortestPath((p1:Person)-[:FRIENDS_WITH*]-(p2:Person))")
642
+ .where(p1: { uuid: person1.uuid })
643
+ .where(p2: { uuid: person2.uuid })
644
+ .return("path, length(path) as distance")
645
+ .first
646
+ end
647
+
648
+ # Recomendaciones basadas en grafo
649
+ def recommend_friends(person, limit: 10)
650
+ Neo4j::ActiveBase.new_query
651
+ .match("(p:Person)-[:FRIENDS_WITH]->(friend)-[:FRIENDS_WITH]->(recommendation:Person)")
652
+ .where(p: { uuid: person.uuid })
653
+ .where_not("(p)-[:FRIENDS_WITH]->(recommendation)")
654
+ .where_not("p = recommendation")
655
+ .return("recommendation, COUNT(friend) as mutual_friends")
656
+ .order("mutual_friends DESC")
657
+ .limit(limit)
658
+ .to_a
659
+ end
660
+
661
+ # Posts que podrian gustarle al usuario
662
+ def recommend_posts(person, limit: 20)
663
+ Neo4j::ActiveBase.new_query
664
+ .match("(p:Person)-[:FRIENDS_WITH]->(friend)-[:LIKES]->(post:Post)")
665
+ .where(p: { uuid: person.uuid })
666
+ .where_not("(p)-[:LIKES]->(post)")
667
+ .with("post, COUNT(friend) as friend_likes")
668
+ .order("friend_likes DESC")
669
+ .limit(limit)
670
+ .return("post, friend_likes")
671
+ .to_a
672
+ end
673
+
674
+ # Influencers (personas con mas conexiones)
675
+ def top_influencers(limit: 10)
676
+ Neo4j::ActiveBase.new_query
677
+ .match("(p:Person)<-[:FOLLOWS]-(follower:Person)")
678
+ .with("p, COUNT(follower) as followers")
679
+ .order("followers DESC")
680
+ .limit(limit)
681
+ .return("p, followers")
682
+ .to_a
683
+ end
684
+ ```
685
+
686
+ ## Elasticsearch
687
+
688
+ ### Configuracion
689
+
690
+ ```ruby
691
+ # Gemfile
692
+ gem "elasticsearch-model"
693
+ gem "elasticsearch-rails"
694
+
695
+ # config/initializers/elasticsearch.rb
696
+ Elasticsearch::Model.client = Elasticsearch::Client.new(
697
+ url: ENV.fetch("ELASTICSEARCH_URL") { "http://localhost:9200" },
698
+ log: Rails.env.development?
699
+ )
700
+ ```
701
+
702
+ ### Modelo con Elasticsearch
703
+
704
+ ```ruby
705
+ # app/models/article.rb
706
+ class Article < ApplicationRecord
707
+ include Elasticsearch::Model
708
+ include Elasticsearch::Model::Callbacks # Sync automatico
709
+
710
+ # Configurar indice
711
+ settings index: {
712
+ number_of_shards: 1,
713
+ analysis: {
714
+ analyzer: {
715
+ spanish_analyzer: {
716
+ type: "spanish",
717
+ stopwords: "_spanish_"
718
+ }
719
+ }
720
+ }
721
+ } do
722
+ mappings dynamic: "false" do
723
+ indexes :title, type: "text", analyzer: "spanish_analyzer", boost: 2
724
+ indexes :body, type: "text", analyzer: "spanish_analyzer"
725
+ indexes :tags, type: "keyword"
726
+ indexes :author_name, type: "text"
727
+ indexes :category, type: "keyword"
728
+ indexes :published_at, type: "date"
729
+ indexes :views_count, type: "integer"
730
+ indexes :location, type: "geo_point"
731
+ end
732
+ end
733
+
734
+ # Definir que datos indexar
735
+ def as_indexed_json(options = {})
736
+ {
737
+ title: title,
738
+ body: body,
739
+ tags: tags,
740
+ author_name: author.name,
741
+ category: category.name,
742
+ published_at: published_at,
743
+ views_count: views_count,
744
+ location: { lat: latitude, lon: longitude }
745
+ }
746
+ end
747
+
748
+ # Metodos de busqueda
749
+ def self.search_query(query, filters = {})
750
+ search_definition = {
751
+ query: {
752
+ bool: {
753
+ must: [
754
+ {
755
+ multi_match: {
756
+ query: query,
757
+ fields: ["title^2", "body", "author_name"],
758
+ type: "best_fields",
759
+ fuzziness: "AUTO"
760
+ }
761
+ }
762
+ ],
763
+ filter: build_filters(filters)
764
+ }
765
+ },
766
+ highlight: {
767
+ fields: {
768
+ title: {},
769
+ body: { fragment_size: 150 }
770
+ }
771
+ },
772
+ aggs: {
773
+ categories: { terms: { field: "category" } },
774
+ tags: { terms: { field: "tags", size: 20 } }
775
+ }
776
+ }
777
+
778
+ __elasticsearch__.search(search_definition)
779
+ end
780
+
781
+ private
782
+
783
+ def self.build_filters(filters)
784
+ result = []
785
+
786
+ if filters[:category].present?
787
+ result << { term: { category: filters[:category] } }
788
+ end
789
+
790
+ if filters[:tags].present?
791
+ result << { terms: { tags: Array(filters[:tags]) } }
792
+ end
793
+
794
+ if filters[:date_range].present?
795
+ result << {
796
+ range: {
797
+ published_at: {
798
+ gte: filters[:date_range][:from],
799
+ lte: filters[:date_range][:to]
800
+ }
801
+ }
802
+ }
803
+ end
804
+
805
+ result
806
+ end
807
+ end
808
+ ```
809
+
810
+ ### Queries avanzadas
811
+
812
+ ```ruby
813
+ # app/services/article_search.rb
814
+ class ArticleSearch
815
+ def initialize(params)
816
+ @query = params[:q]
817
+ @page = params[:page] || 1
818
+ @per_page = params[:per_page] || 20
819
+ @filters = params.slice(:category, :tags, :author)
820
+ @sort = params[:sort]
821
+ end
822
+
823
+ def call
824
+ Article.__elasticsearch__.search(search_body)
825
+ end
826
+
827
+ private
828
+
829
+ def search_body
830
+ {
831
+ query: build_query,
832
+ from: (@page - 1) * @per_page,
833
+ size: @per_page,
834
+ sort: build_sort,
835
+ aggs: aggregations,
836
+ highlight: highlights,
837
+ suggest: suggestions
838
+ }
839
+ end
840
+
841
+ def build_query
842
+ {
843
+ bool: {
844
+ must: must_clauses,
845
+ filter: filter_clauses,
846
+ should: boost_clauses
847
+ }
848
+ }
849
+ end
850
+
851
+ def must_clauses
852
+ return [{ match_all: {} }] if @query.blank?
853
+
854
+ [
855
+ {
856
+ multi_match: {
857
+ query: @query,
858
+ fields: ["title^3", "body", "tags^2"],
859
+ type: "cross_fields",
860
+ operator: "and"
861
+ }
862
+ }
863
+ ]
864
+ end
865
+
866
+ def filter_clauses
867
+ filters = []
868
+
869
+ filters << { term: { category: @filters[:category] } } if @filters[:category]
870
+ filters << { terms: { tags: @filters[:tags] } } if @filters[:tags]
871
+ filters << { term: { "author_name.keyword": @filters[:author] } } if @filters[:author]
872
+
873
+ filters
874
+ end
875
+
876
+ def boost_clauses
877
+ [
878
+ { range: { published_at: { gte: "now-7d", boost: 2 } } }, # Articulos recientes
879
+ { range: { views_count: { gte: 1000, boost: 1.5 } } } # Populares
880
+ ]
881
+ end
882
+
883
+ def build_sort
884
+ case @sort
885
+ when "recent"
886
+ [{ published_at: "desc" }]
887
+ when "popular"
888
+ [{ views_count: "desc" }]
889
+ else
890
+ ["_score", { published_at: "desc" }]
891
+ end
892
+ end
893
+
894
+ def aggregations
895
+ {
896
+ categories: {
897
+ terms: { field: "category", size: 10 }
898
+ },
899
+ popular_tags: {
900
+ terms: { field: "tags", size: 20 }
901
+ },
902
+ date_histogram: {
903
+ date_histogram: {
904
+ field: "published_at",
905
+ calendar_interval: "month"
906
+ }
907
+ }
908
+ }
909
+ end
910
+
911
+ def highlights
912
+ {
913
+ pre_tags: ["<mark>"],
914
+ post_tags: ["</mark>"],
915
+ fields: {
916
+ title: { number_of_fragments: 0 },
917
+ body: { fragment_size: 200, number_of_fragments: 3 }
918
+ }
919
+ }
920
+ end
921
+
922
+ def suggestions
923
+ return {} if @query.blank?
924
+
925
+ {
926
+ title_suggestion: {
927
+ text: @query,
928
+ term: { field: "title" }
929
+ }
930
+ }
931
+ end
932
+ end
933
+ ```
934
+
935
+ ## DynamoDB
936
+
937
+ ### Configuracion
938
+
939
+ ```ruby
940
+ # Gemfile
941
+ gem "aws-sdk-dynamodb"
942
+ gem "dynamoid" # ORM para DynamoDB
943
+
944
+ # config/initializers/dynamodb.rb
945
+ Dynamoid.configure do |config|
946
+ config.namespace = "myapp_#{Rails.env}"
947
+ config.region = ENV.fetch("AWS_REGION") { "us-east-1" }
948
+ config.access_key = ENV["AWS_ACCESS_KEY_ID"]
949
+ config.secret_key = ENV["AWS_SECRET_ACCESS_KEY"]
950
+ end
951
+ ```
952
+
953
+ ### Modelos con Dynamoid
954
+
955
+ ```ruby
956
+ # app/models/session.rb
957
+ class Session
958
+ include Dynamoid::Document
959
+
960
+ table name: :sessions, key: :session_id
961
+
962
+ field :session_id, :string
963
+ field :user_id, :string
964
+ field :data, :serialized
965
+ field :expires_at, :datetime
966
+
967
+ global_secondary_index hash_key: :user_id, projected_attributes: :all
968
+
969
+ # TTL para auto-expiracion
970
+ field :ttl, :integer
971
+
972
+ before_create :set_ttl
973
+
974
+ private
975
+
976
+ def set_ttl
977
+ self.ttl = (expires_at || 1.day.from_now).to_i
978
+ end
979
+ end
980
+
981
+ # app/models/event.rb
982
+ class Event
983
+ include Dynamoid::Document
984
+
985
+ table name: :events, key: :event_id, range_key: :timestamp
986
+
987
+ field :event_id, :string
988
+ field :timestamp, :datetime
989
+ field :event_type, :string
990
+ field :user_id, :string
991
+ field :data, :map
992
+ field :metadata, :map
993
+
994
+ global_secondary_index hash_key: :user_id,
995
+ range_key: :timestamp,
996
+ projected_attributes: :all
997
+
998
+ global_secondary_index hash_key: :event_type,
999
+ range_key: :timestamp,
1000
+ projected_attributes: :keys_only
1001
+ end
1002
+ ```
1003
+
1004
+ ### Queries en DynamoDB
1005
+
1006
+ ```ruby
1007
+ # Buscar por primary key
1008
+ session = Session.find("session-123")
1009
+
1010
+ # Query por GSI
1011
+ events = Event.where(user_id: "user-123")
1012
+ .where("timestamp.gt": 1.day.ago)
1013
+
1014
+ # Scan con filtro (costoso, evitar en produccion)
1015
+ sessions = Session.scan_filter(
1016
+ :user_id => { eq: "user-123" },
1017
+ :expires_at => { gt: Time.current }
1018
+ )
1019
+
1020
+ # Batch operations
1021
+ Session.import([session1, session2, session3])
1022
+ Session.find_all(["id1", "id2", "id3"])
1023
+
1024
+ # Operaciones condicionales
1025
+ session.update_attributes(
1026
+ { data: new_data },
1027
+ conditions: { if: { version: current_version } }
1028
+ )
1029
+ ```
1030
+
1031
+ ## Cuando Usar NoSQL vs SQL
1032
+
1033
+ ```markdown
1034
+ ## Usar SQL cuando:
1035
+ - Necesitas transacciones ACID complejas
1036
+ - Datos altamente relacionados
1037
+ - Queries ad-hoc complejos
1038
+ - Reporting y analytics
1039
+ - Schema estricto es importante
1040
+
1041
+ ## Usar MongoDB cuando:
1042
+ - Schema flexible/evolutivo
1043
+ - Documentos anidados complejos
1044
+ - Datos semi-estructurados
1045
+ - Prototipado rapido
1046
+ - Catalogos de productos
1047
+
1048
+ ## Usar Redis cuando:
1049
+ - Cache de alto rendimiento
1050
+ - Sessions
1051
+ - Colas y pub/sub
1052
+ - Leaderboards
1053
+ - Rate limiting
1054
+ - Datos efimeros
1055
+
1056
+ ## Usar Elasticsearch cuando:
1057
+ - Full-text search
1058
+ - Log analytics
1059
+ - Busqueda facetada
1060
+ - Autocompletado
1061
+ - Geo-search
1062
+
1063
+ ## Usar Neo4j cuando:
1064
+ - Redes sociales
1065
+ - Sistemas de recomendacion
1066
+ - Deteccion de fraude
1067
+ - Knowledge graphs
1068
+ - Path finding
1069
+
1070
+ ## Usar DynamoDB cuando:
1071
+ - Escala masiva (millones de requests/segundo)
1072
+ - Latencia consistente
1073
+ - Serverless
1074
+ - Key-value simple con indices
1075
+ - Gaming/IoT
1076
+ ```
1077
+
1078
+ ## Patrones Hibridos
1079
+
1080
+ ```ruby
1081
+ # Usar SQL como fuente de verdad + Redis para cache
1082
+ class Product < ApplicationRecord
1083
+ after_commit :invalidate_cache
1084
+
1085
+ def self.featured
1086
+ Rails.cache.fetch("products:featured", expires_in: 1.hour) do
1087
+ where(featured: true).limit(10).to_a
1088
+ end
1089
+ end
1090
+
1091
+ private
1092
+
1093
+ def invalidate_cache
1094
+ Rails.cache.delete("products:featured")
1095
+ end
1096
+ end
1097
+
1098
+ # Usar SQL + Elasticsearch para busqueda
1099
+ class Product < ApplicationRecord
1100
+ include Elasticsearch::Model
1101
+
1102
+ # SQL para transacciones y relaciones
1103
+ # Elasticsearch para busqueda
1104
+
1105
+ def self.search(query)
1106
+ if query.present?
1107
+ __elasticsearch__.search(query).records
1108
+ else
1109
+ all
1110
+ end
1111
+ end
1112
+ end
1113
+
1114
+ # Usar SQL + MongoDB para diferentes partes
1115
+ class Order < ApplicationRecord
1116
+ # Datos transaccionales en PostgreSQL
1117
+ has_many :line_items
1118
+
1119
+ # Logs/eventos en MongoDB
1120
+ def log_event(event_type, data = {})
1121
+ OrderEvent.create!(
1122
+ order_id: id.to_s,
1123
+ event_type: event_type,
1124
+ data: data,
1125
+ timestamp: Time.current
1126
+ )
1127
+ end
1128
+ end
1129
+ ```
1130
+
1131
+ ## Checklist
1132
+
1133
+ - [ ] Evaluar si NoSQL es apropiado para el caso de uso
1134
+ - [ ] Considerar patron hibrido SQL + NoSQL
1135
+ - [ ] Configurar conexiones con pooling
1136
+ - [ ] Implementar manejo de errores y reintentos
1137
+ - [ ] Configurar indices apropiados
1138
+ - [ ] Establecer TTLs para datos temporales
1139
+ - [ ] Monitorear rendimiento y uso de memoria
1140
+ - [ ] Planificar estrategia de backup
1141
+ - [ ] Documentar decisiones de modelado